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      public 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<Item>();
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, Integer>( 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( "", 0 );
202             QUALIFIERS.put( "sp", 1 );
203         }
204 
205         private final String version;
206 
207         private int index;
208 
209         private String token;
210 
211         private boolean number;
212 
213         private boolean terminatedByNumber;
214 
215         public Tokenizer( String version )
216         {
217             this.version = ( version.length() > 0 ) ? version : "0";
218         }
219 
220         public boolean next()
221         {
222             final int n = version.length();
223             if ( index >= n )
224             {
225                 return false;
226             }
227 
228             int state = -2;
229 
230             int start = index;
231             int end = n;
232             terminatedByNumber = false;
233 
234             for ( ; index < n; index++ )
235             {
236                 char c = version.charAt( index );
237 
238                 if ( c == '.' || c == '-' || c == '_' )
239                 {
240                     end = index;
241                     index++;
242                     break;
243                 }
244                 else
245                 {
246                     int digit = Character.digit( c, 10 );
247                     if ( digit >= 0 )
248                     {
249                         if ( state == -1 )
250                         {
251                             end = index;
252                             terminatedByNumber = true;
253                             break;
254                         }
255                         if ( state == 0 )
256                         {
257                             // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling)
258                             start++;
259                         }
260                         state = ( state > 0 || digit > 0 ) ? 1 : 0;
261                     }
262                     else
263                     {
264                         if ( state >= 0 )
265                         {
266                             end = index;
267                             break;
268                         }
269                         state = -1;
270                     }
271                 }
272 
273             }
274 
275             if ( end - start > 0 )
276             {
277                 token = version.substring( start, end );
278                 number = state >= 0;
279             }
280             else
281             {
282                 token = "0";
283                 number = true;
284             }
285 
286             return true;
287         }
288 
289         @Override
290         public String toString()
291         {
292             return String.valueOf( token );
293         }
294 
295         public Item toItem()
296         {
297             if ( number )
298             {
299                 try
300                 {
301                     if ( token.length() < 10 )
302                     {
303                         return new Item( Item.KIND_INT, Integer.parseInt( token ) );
304                     }
305                     else
306                     {
307                         return new Item( Item.KIND_BIGINT, new BigInteger( token ) );
308                     }
309                 }
310                 catch ( NumberFormatException e )
311                 {
312                     throw new IllegalStateException( e );
313                 }
314             }
315             else
316             {
317                 if ( index >= version.length() )
318                 {
319                     if ( "min".equalsIgnoreCase( token ) )
320                     {
321                         return Item.MIN;
322                     }
323                     else if ( "max".equalsIgnoreCase( token ) )
324                     {
325                         return Item.MAX;
326                     }
327                 }
328                 if ( terminatedByNumber && token.length() == 1 )
329                 {
330                     switch ( token.charAt( 0 ) )
331                     {
332                         case 'a':
333                         case 'A':
334                             return new Item( Item.KIND_QUALIFIER, QUALIFIER_ALPHA );
335                         case 'b':
336                         case 'B':
337                             return new Item( Item.KIND_QUALIFIER, QUALIFIER_BETA );
338                         case 'm':
339                         case 'M':
340                             return new Item( Item.KIND_QUALIFIER, QUALIFIER_MILESTONE );
341                         default:
342                     }
343                 }
344                 Integer qualifier = QUALIFIERS.get( token );
345                 if ( qualifier != null )
346                 {
347                     return new Item( Item.KIND_QUALIFIER, qualifier );
348                 }
349                 else
350                 {
351                     return new Item( Item.KIND_STRING, token.toLowerCase( Locale.ENGLISH ) );
352                 }
353             }
354         }
355 
356     }
357 
358     static final class Item
359     {
360 
361         static final int KIND_MAX = 8;
362 
363         static final int KIND_BIGINT = 5;
364 
365         static final int KIND_INT = 4;
366 
367         static final int KIND_STRING = 3;
368 
369         static final int KIND_QUALIFIER = 2;
370 
371         static final int KIND_MIN = 0;
372 
373         static final Item MAX = new Item( KIND_MAX, "max" );
374 
375         static final Item MIN = new Item( KIND_MIN, "min" );
376 
377         private final int kind;
378 
379         private final Object value;
380 
381         public Item( int kind, Object value )
382         {
383             this.kind = kind;
384             this.value = value;
385         }
386 
387         public boolean isNumber()
388         {
389             return ( kind & KIND_QUALIFIER ) == 0; // i.e. kind != string/qualifier
390         }
391 
392         public int compareTo( Item that )
393         {
394             int rel;
395             if ( that == null )
396             {
397                 // null in this context denotes the pad item (0 or "ga")
398                 switch ( kind )
399                 {
400                     case KIND_MIN:
401                         rel = -1;
402                         break;
403                     case KIND_MAX:
404                     case KIND_BIGINT:
405                     case KIND_STRING:
406                         rel = 1;
407                         break;
408                     case KIND_INT:
409                     case KIND_QUALIFIER:
410                         rel = (Integer) value;
411                         break;
412                     default:
413                         throw new IllegalStateException( "unknown version item kind " + kind );
414                 }
415             }
416             else
417             {
418                 rel = kind - that.kind;
419                 if ( rel == 0 )
420                 {
421                     switch ( kind )
422                     {
423                         case KIND_MAX:
424                         case KIND_MIN:
425                             break;
426                         case KIND_BIGINT:
427                             rel = ( (BigInteger) value ).compareTo( (BigInteger) that.value );
428                             break;
429                         case KIND_INT:
430                         case KIND_QUALIFIER:
431                             rel = ( (Integer) value ).compareTo( (Integer) that.value );
432                             break;
433                         case KIND_STRING:
434                             rel = ( (String) value ).compareToIgnoreCase( (String) that.value );
435                             break;
436                         default:
437                             throw new IllegalStateException( "unknown version item kind " + kind );
438                     }
439                 }
440             }
441             return rel;
442         }
443 
444         @Override
445         public boolean equals( Object obj )
446         {
447             return ( obj instanceof Item ) && compareTo( (Item) obj ) == 0;
448         }
449 
450         @Override
451         public int hashCode()
452         {
453             return value.hashCode() + kind * 31;
454         }
455 
456         @Override
457         public String toString()
458         {
459             return String.valueOf( value );
460         }
461 
462     }
463 
464 }