View Javadoc
1   package org.eclipse.aether.util.version;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   * 
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   * 
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.math.BigInteger;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.TreeMap;
29  
30  import org.eclipse.aether.version.Version;
31  
32  /**
33   * A generic version, that is a version that accepts any input string and tries to apply common sense sorting. See
34   * {@link GenericVersionScheme} for details.
35   */
36  final class GenericVersion
37      implements Version
38  {
39  
40      private final String version;
41  
42      private final Item[] items;
43  
44      private final int hash;
45  
46      /**
47       * Creates a generic version from the specified string.
48       * 
49       * @param version The version string, must not be {@code null}.
50       */
51      GenericVersion( String version )
52      {
53          this.version = version;
54          items = parse( version );
55          hash = Arrays.hashCode( items );
56      }
57  
58      private static Item[] parse( String version )
59      {
60          List<Item> items = new ArrayList<>();
61  
62          for ( Tokenizer tokenizer = new Tokenizer( version ); tokenizer.next(); )
63          {
64              Item item = tokenizer.toItem();
65              items.add( item );
66          }
67  
68          trimPadding( items );
69  
70          return items.toArray( new Item[items.size()] );
71      }
72  
73      private static void trimPadding( List<Item> items )
74      {
75          Boolean number = null;
76          int end = items.size() - 1;
77          for ( int i = end; i > 0; i-- )
78          {
79              Item item = items.get( i );
80              if ( !Boolean.valueOf( item.isNumber() ).equals( number ) )
81              {
82                  end = i;
83                  number = item.isNumber();
84              }
85              if ( end == i && ( i == items.size() - 1 || items.get( i - 1 ).isNumber() == item.isNumber() )
86                  && item.compareTo( null ) == 0 )
87              {
88                  items.remove( i );
89                  end--;
90              }
91          }
92      }
93  
94      public int compareTo( Version obj )
95      {
96          final Item[] these = items;
97          final Item[] those = ( (GenericVersion) obj ).items;
98  
99          boolean number = true;
100 
101         for ( int index = 0;; index++ )
102         {
103             if ( index >= these.length && index >= those.length )
104             {
105                 return 0;
106             }
107             else if ( index >= these.length )
108             {
109                 return -comparePadding( those, index, null );
110             }
111             else if ( index >= those.length )
112             {
113                 return comparePadding( these, index, null );
114             }
115 
116             Item thisItem = these[index];
117             Item thatItem = those[index];
118 
119             if ( thisItem.isNumber() != thatItem.isNumber() )
120             {
121                 if ( number == thisItem.isNumber() )
122                 {
123                     return comparePadding( these, index, number );
124                 }
125                 else
126                 {
127                     return -comparePadding( those, index, number );
128                 }
129             }
130             else
131             {
132                 int rel = thisItem.compareTo( thatItem );
133                 if ( rel != 0 )
134                 {
135                     return rel;
136                 }
137                 number = thisItem.isNumber();
138             }
139         }
140     }
141 
142     private static int comparePadding( Item[] items, int index, Boolean number )
143     {
144         int rel = 0;
145         for ( int i = index; i < items.length; i++ )
146         {
147             Item item = items[i];
148             if ( number != null && number != item.isNumber() )
149             {
150                 break;
151             }
152             rel = item.compareTo( null );
153             if ( rel != 0 )
154             {
155                 break;
156             }
157         }
158         return rel;
159     }
160 
161     @Override
162     public boolean equals( Object obj )
163     {
164         return ( obj instanceof GenericVersion ) && compareTo( (GenericVersion) obj ) == 0;
165     }
166 
167     @Override
168     public int hashCode()
169     {
170         return hash;
171     }
172 
173     @Override
174     public String toString()
175     {
176         return version;
177     }
178 
179     static final class Tokenizer
180     {
181 
182         private static final Integer QUALIFIER_ALPHA = -5;
183 
184         private static final Integer QUALIFIER_BETA = -4;
185 
186         private static final Integer QUALIFIER_MILESTONE = -3;
187 
188         private static final Map<String, Integer> QUALIFIERS;
189 
190         static
191         {
192             QUALIFIERS = new TreeMap<>( String.CASE_INSENSITIVE_ORDER );
193             QUALIFIERS.put( "alpha", QUALIFIER_ALPHA );
194             QUALIFIERS.put( "beta", QUALIFIER_BETA );
195             QUALIFIERS.put( "milestone", QUALIFIER_MILESTONE );
196             QUALIFIERS.put( "cr", -2 );
197             QUALIFIERS.put( "rc", -2 );
198             QUALIFIERS.put( "snapshot", -1 );
199             QUALIFIERS.put( "ga", 0 );
200             QUALIFIERS.put( "final", 0 );
201             QUALIFIERS.put( "release", 0 );
202             QUALIFIERS.put( "", 0 );
203             QUALIFIERS.put( "sp", 1 );
204         }
205 
206         private final String version;
207 
208         private int index;
209 
210         private String token;
211 
212         private boolean number;
213 
214         private boolean terminatedByNumber;
215 
216         Tokenizer( String version )
217         {
218             this.version = ( version.length() > 0 ) ? version : "0";
219         }
220 
221         public boolean next()
222         {
223             final int n = version.length();
224             if ( index >= n )
225             {
226                 return false;
227             }
228 
229             int state = -2;
230 
231             int start = index;
232             int end = n;
233             terminatedByNumber = false;
234 
235             for ( ; index < n; index++ )
236             {
237                 char c = version.charAt( index );
238 
239                 if ( c == '.' || c == '-' || c == '_' )
240                 {
241                     end = index;
242                     index++;
243                     break;
244                 }
245                 else
246                 {
247                     int digit = Character.digit( c, 10 );
248                     if ( digit >= 0 )
249                     {
250                         if ( state == -1 )
251                         {
252                             end = index;
253                             terminatedByNumber = true;
254                             break;
255                         }
256                         if ( state == 0 )
257                         {
258                             // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling)
259                             start++;
260                         }
261                         state = ( state > 0 || digit > 0 ) ? 1 : 0;
262                     }
263                     else
264                     {
265                         if ( state >= 0 )
266                         {
267                             end = index;
268                             break;
269                         }
270                         state = -1;
271                     }
272                 }
273 
274             }
275 
276             if ( end - start > 0 )
277             {
278                 token = version.substring( start, end );
279                 number = state >= 0;
280             }
281             else
282             {
283                 token = "0";
284                 number = true;
285             }
286 
287             return true;
288         }
289 
290         @Override
291         public String toString()
292         {
293             return String.valueOf( token );
294         }
295 
296         public Item toItem()
297         {
298             if ( number )
299             {
300                 try
301                 {
302                     if ( token.length() < 10 )
303                     {
304                         return new Item( Item.KIND_INT, Integer.parseInt( token ) );
305                     }
306                     else
307                     {
308                         return new Item( Item.KIND_BIGINT, new BigInteger( token ) );
309                     }
310                 }
311                 catch ( NumberFormatException e )
312                 {
313                     throw new IllegalStateException( e );
314                 }
315             }
316             else
317             {
318                 if ( index >= version.length() )
319                 {
320                     if ( "min".equalsIgnoreCase( token ) )
321                     {
322                         return Item.MIN;
323                     }
324                     else if ( "max".equalsIgnoreCase( token ) )
325                     {
326                         return Item.MAX;
327                     }
328                 }
329                 if ( terminatedByNumber && token.length() == 1 )
330                 {
331                     switch ( token.charAt( 0 ) )
332                     {
333                         case 'a':
334                         case 'A':
335                             return new Item( Item.KIND_QUALIFIER, QUALIFIER_ALPHA );
336                         case 'b':
337                         case 'B':
338                             return new Item( Item.KIND_QUALIFIER, QUALIFIER_BETA );
339                         case 'm':
340                         case 'M':
341                             return new Item( Item.KIND_QUALIFIER, QUALIFIER_MILESTONE );
342                         default:
343                     }
344                 }
345                 Integer qualifier = QUALIFIERS.get( token );
346                 if ( qualifier != null )
347                 {
348                     return new Item( Item.KIND_QUALIFIER, qualifier );
349                 }
350                 else
351                 {
352                     return new Item( Item.KIND_STRING, token.toLowerCase( Locale.ENGLISH ) );
353                 }
354             }
355         }
356 
357     }
358 
359     static final class Item
360     {
361 
362         static final int KIND_MAX = 8;
363 
364         static final int KIND_BIGINT = 5;
365 
366         static final int KIND_INT = 4;
367 
368         static final int KIND_STRING = 3;
369 
370         static final int KIND_QUALIFIER = 2;
371 
372         static final int KIND_MIN = 0;
373 
374         static final Item MAX = new Item( KIND_MAX, "max" );
375 
376         static final Item MIN = new Item( KIND_MIN, "min" );
377 
378         private final int kind;
379 
380         private final Object value;
381 
382         Item( int kind, Object value )
383         {
384             this.kind = kind;
385             this.value = value;
386         }
387 
388         public boolean isNumber()
389         {
390             return ( kind & KIND_QUALIFIER ) == 0; // i.e. kind != string/qualifier
391         }
392 
393         public int compareTo( Item that )
394         {
395             int rel;
396             if ( that == null )
397             {
398                 // null in this context denotes the pad item (0 or "ga")
399                 switch ( kind )
400                 {
401                     case KIND_MIN:
402                         rel = -1;
403                         break;
404                     case KIND_MAX:
405                     case KIND_BIGINT:
406                     case KIND_STRING:
407                         rel = 1;
408                         break;
409                     case KIND_INT:
410                     case KIND_QUALIFIER:
411                         rel = (Integer) value;
412                         break;
413                     default:
414                         throw new IllegalStateException( "unknown version item kind " + kind );
415                 }
416             }
417             else
418             {
419                 rel = kind - that.kind;
420                 if ( rel == 0 )
421                 {
422                     switch ( kind )
423                     {
424                         case KIND_MAX:
425                         case KIND_MIN:
426                             break;
427                         case KIND_BIGINT:
428                             rel = ( (BigInteger) value ).compareTo( (BigInteger) that.value );
429                             break;
430                         case KIND_INT:
431                         case KIND_QUALIFIER:
432                             rel = ( (Integer) value ).compareTo( (Integer) that.value );
433                             break;
434                         case KIND_STRING:
435                             rel = ( (String) value ).compareToIgnoreCase( (String) that.value );
436                             break;
437                         default:
438                             throw new IllegalStateException( "unknown version item kind " + kind );
439                     }
440                 }
441             }
442             return rel;
443         }
444 
445         @Override
446         public boolean equals( Object obj )
447         {
448             return ( obj instanceof Item ) && compareTo( (Item) obj ) == 0;
449         }
450 
451         @Override
452         public int hashCode()
453         {
454             return value.hashCode() + kind * 31;
455         }
456 
457         @Override
458         public String toString()
459         {
460             return String.valueOf( value );
461         }
462 
463     }
464 
465 }