001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019
020 package org.jetbrains.kotlin.config;
021
022 import java.math.BigInteger;
023 import java.util.ArrayList;
024 import java.util.Arrays;
025 import java.util.Iterator;
026 import java.util.List;
027 import java.util.Locale;
028 import java.util.Properties;
029 import java.util.Stack;
030
031 /**
032 * Generic implementation of version comparison.
033 *
034 * <p>Features:
035 * <ul>
036 * <li>mixing of '<code>-</code>' (dash) and '<code>.</code>' (dot) separators,</li>
037 * <li>transition between characters and digits also constitutes a separator:
038 * <code>1.0alpha1 => [1, 0, alpha, 1]</code></li>
039 * <li>unlimited number of version components,</li>
040 * <li>version components in the text can be digits or strings,</li>
041 * <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
042 * Well-known qualifiers (case insensitive) are:<ul>
043 * <li><code>alpha</code> or <code>a</code></li>
044 * <li><code>beta</code> or <code>b</code></li>
045 * <li><code>milestone</code> or <code>m</code></li>
046 * <li><code>rc</code> or <code>cr</code></li>
047 * <li><code>snapshot</code></li>
048 * <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li>
049 * <li><code>sp</code></li>
050 * </ul>
051 * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive),
052 * </li>
053 * <li>a dash usually precedes a qualifier, and is always less important than something preceded with a dot.</li>
054 * </ul></p>
055 *
056 * @see <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning" on Maven Wiki</a>
057 * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
058 * @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
059 */
060 @SuppressWarnings("ALL")
061 public class MavenComparableVersion
062 implements Comparable<MavenComparableVersion>
063 {
064 private String value;
065
066 private String canonical;
067
068 private ListItem items;
069
070 private interface Item
071 {
072 int INTEGER_ITEM = 0;
073 int STRING_ITEM = 1;
074 int LIST_ITEM = 2;
075
076 int compareTo( Item item );
077
078 int getType();
079
080 boolean isNull();
081 }
082
083 /**
084 * Represents a numeric item in the version item list.
085 */
086 private static class IntegerItem
087 implements Item
088 {
089 private static final BigInteger BIG_INTEGER_ZERO = new BigInteger( "0" );
090
091 private final BigInteger value;
092
093 public static final IntegerItem ZERO = new IntegerItem();
094
095 private IntegerItem()
096 {
097 this.value = BIG_INTEGER_ZERO;
098 }
099
100 public IntegerItem( String str )
101 {
102 this.value = new BigInteger( str );
103 }
104
105 public int getType()
106 {
107 return INTEGER_ITEM;
108 }
109
110 public boolean isNull()
111 {
112 return BIG_INTEGER_ZERO.equals( value );
113 }
114
115 public int compareTo( Item item )
116 {
117 if ( item == null )
118 {
119 return BIG_INTEGER_ZERO.equals( value ) ? 0 : 1; // 1.0 == 1, 1.1 > 1
120 }
121
122 switch ( item.getType() )
123 {
124 case INTEGER_ITEM:
125 return value.compareTo( ( (IntegerItem) item ).value );
126
127 case STRING_ITEM:
128 return 1; // 1.1 > 1-sp
129
130 case LIST_ITEM:
131 return 1; // 1.1 > 1-1
132
133 default:
134 throw new RuntimeException( "invalid item: " + item.getClass() );
135 }
136 }
137
138 public String toString()
139 {
140 return value.toString();
141 }
142 }
143
144 /**
145 * Represents a string in the version item list, usually a qualifier.
146 */
147 private static class StringItem
148 implements Item
149 {
150 private static final String[] QUALIFIERS = { "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" };
151
152 @SuppressWarnings( "checkstyle:constantname" )
153 private static final List<String> _QUALIFIERS = Arrays.asList( QUALIFIERS );
154
155 private static final Properties ALIASES = new Properties();
156 static
157 {
158 ALIASES.put( "ga", "" );
159 ALIASES.put( "final", "" );
160 ALIASES.put( "cr", "rc" );
161 }
162
163 /**
164 * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
165 * the version older than one without a qualifier, or more recent.
166 */
167 private static final String RELEASE_VERSION_INDEX = String.valueOf( _QUALIFIERS.indexOf( "" ) );
168
169 private String value;
170
171 public StringItem( String value, boolean followedByDigit )
172 {
173 if ( followedByDigit && value.length() == 1 )
174 {
175 // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
176 switch ( value.charAt( 0 ) )
177 {
178 case 'a':
179 value = "alpha";
180 break;
181 case 'b':
182 value = "beta";
183 break;
184 case 'm':
185 value = "milestone";
186 break;
187 default:
188 }
189 }
190 this.value = ALIASES.getProperty( value , value );
191 }
192
193 public int getType()
194 {
195 return STRING_ITEM;
196 }
197
198 public boolean isNull()
199 {
200 return ( comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX ) == 0 );
201 }
202
203 /**
204 * Returns a comparable value for a qualifier.
205 *
206 * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical
207 * ordering.
208 *
209 * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1
210 * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character,
211 * so this is still fast. If more characters are needed then it requires a lexical sort anyway.
212 *
213 * @param qualifier
214 * @return an equivalent value that can be used with lexical comparison
215 */
216 public static String comparableQualifier( String qualifier )
217 {
218 int i = _QUALIFIERS.indexOf( qualifier );
219
220 return i == -1 ? ( _QUALIFIERS.size() + "-" + qualifier ) : String.valueOf( i );
221 }
222
223 public int compareTo( Item item )
224 {
225 if ( item == null )
226 {
227 // 1-rc < 1, 1-ga > 1
228 return comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX );
229 }
230 switch ( item.getType() )
231 {
232 case INTEGER_ITEM:
233 return -1; // 1.any < 1.1 ?
234
235 case STRING_ITEM:
236 return comparableQualifier( value ).compareTo( comparableQualifier( ( (StringItem) item ).value ) );
237
238 case LIST_ITEM:
239 return -1; // 1.any < 1-1
240
241 default:
242 throw new RuntimeException( "invalid item: " + item.getClass() );
243 }
244 }
245
246 public String toString()
247 {
248 return value;
249 }
250 }
251
252 /**
253 * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
254 * with '-(number)' in the version specification).
255 */
256 private static class ListItem
257 extends ArrayList<Item>
258 implements Item
259 {
260 public int getType()
261 {
262 return LIST_ITEM;
263 }
264
265 public boolean isNull()
266 {
267 return ( size() == 0 );
268 }
269
270 void normalize()
271 {
272 for ( int i = size() - 1; i >= 0; i-- )
273 {
274 Item lastItem = get( i );
275
276 if ( lastItem.isNull() )
277 {
278 // remove null trailing items: 0, "", empty list
279 remove( i );
280 }
281 else if ( !( lastItem instanceof ListItem ) )
282 {
283 break;
284 }
285 }
286 }
287
288 public int compareTo( Item item )
289 {
290 if ( item == null )
291 {
292 if ( size() == 0 )
293 {
294 return 0; // 1-0 = 1- (normalize) = 1
295 }
296 Item first = get( 0 );
297 return first.compareTo( null );
298 }
299 switch ( item.getType() )
300 {
301 case INTEGER_ITEM:
302 return -1; // 1-1 < 1.0.x
303
304 case STRING_ITEM:
305 return 1; // 1-1 > 1-sp
306
307 case LIST_ITEM:
308 Iterator<Item> left = iterator();
309 Iterator<Item> right = ( (ListItem) item ).iterator();
310
311 while ( left.hasNext() || right.hasNext() )
312 {
313 Item l = left.hasNext() ? left.next() : null;
314 Item r = right.hasNext() ? right.next() : null;
315
316 // if this is shorter, then invert the compare and mul with -1
317 int result = l == null ? ( r == null ? 0 : -1 * r.compareTo( l ) ) : l.compareTo( r );
318
319 if ( result != 0 )
320 {
321 return result;
322 }
323 }
324
325 return 0;
326
327 default:
328 throw new RuntimeException( "invalid item: " + item.getClass() );
329 }
330 }
331
332 public String toString()
333 {
334 StringBuilder buffer = new StringBuilder();
335 for ( Iterator<Item> iter = iterator(); iter.hasNext(); )
336 {
337 Item item = iter.next();
338 if ( buffer.length() > 0 )
339 {
340 buffer.append( ( item instanceof ListItem ) ? '-' : '.' );
341 }
342 buffer.append( item );
343 }
344 return buffer.toString();
345 }
346 }
347
348 public MavenComparableVersion( String version )
349 {
350 parseVersion( version );
351 }
352
353 public final void parseVersion( String version )
354 {
355 this.value = version;
356
357 items = new ListItem();
358
359 version = version.toLowerCase( Locale.ENGLISH );
360
361 ListItem list = items;
362
363 Stack<Item> stack = new Stack<Item>();
364 stack.push( list );
365
366 boolean isDigit = false;
367
368 int startIndex = 0;
369
370 for ( int i = 0; i < version.length(); i++ )
371 {
372 char c = version.charAt( i );
373
374 if ( c == '.' )
375 {
376 if ( i == startIndex )
377 {
378 list.add( IntegerItem.ZERO );
379 }
380 else
381 {
382 list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
383 }
384 startIndex = i + 1;
385 }
386 else if ( c == '-' )
387 {
388 if ( i == startIndex )
389 {
390 list.add( IntegerItem.ZERO );
391 }
392 else
393 {
394 list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
395 }
396 startIndex = i + 1;
397
398 list.add( list = new ListItem() );
399 stack.push( list );
400 }
401 else if ( Character.isDigit( c ) )
402 {
403 if ( !isDigit && i > startIndex )
404 {
405 list.add( new StringItem( version.substring( startIndex, i ), true ) );
406 startIndex = i;
407
408 list.add( list = new ListItem() );
409 stack.push( list );
410 }
411
412 isDigit = true;
413 }
414 else
415 {
416 if ( isDigit && i > startIndex )
417 {
418 list.add( parseItem( true, version.substring( startIndex, i ) ) );
419 startIndex = i;
420
421 list.add( list = new ListItem() );
422 stack.push( list );
423 }
424
425 isDigit = false;
426 }
427 }
428
429 if ( version.length() > startIndex )
430 {
431 list.add( parseItem( isDigit, version.substring( startIndex ) ) );
432 }
433
434 while ( !stack.isEmpty() )
435 {
436 list = (ListItem) stack.pop();
437 list.normalize();
438 }
439
440 canonical = items.toString();
441 }
442
443 private static Item parseItem( boolean isDigit, String buf )
444 {
445 return isDigit ? new IntegerItem( buf ) : new StringItem( buf, false );
446 }
447
448 public int compareTo( MavenComparableVersion o )
449 {
450 return items.compareTo( o.items );
451 }
452
453 public String toString()
454 {
455 return value;
456 }
457
458 public String getCanonical()
459 {
460 return canonical;
461 }
462
463 public boolean equals( Object o )
464 {
465 return ( o instanceof MavenComparableVersion) && canonical.equals(((MavenComparableVersion) o ).canonical );
466 }
467
468 public int hashCode()
469 {
470 return canonical.hashCode();
471 }
472
473 /**
474 * Main to test version parsing and comparison.
475 *
476 * @param args the version strings to parse and compare
477 */
478 public static void main( String... args )
479 {
480 System.out.println( "Display parameters as parsed by Maven (in canonical form) and comparison result:" );
481 if ( args.length == 0 )
482 {
483 return;
484 }
485
486 MavenComparableVersion prev = null;
487 int i = 1;
488 for ( String version : args )
489 {
490 MavenComparableVersion c = new MavenComparableVersion(version );
491
492 if ( prev != null )
493 {
494 int compare = prev.compareTo( c );
495 System.out.println( " " + prev.toString() + ' '
496 + ( ( compare == 0 ) ? "==" : ( ( compare < 0 ) ? "<" : ">" ) ) + ' ' + version );
497 }
498
499 System.out.println( String.valueOf( i++ ) + ". " + version + " == " + c.getCanonical() );
500
501 prev = c;
502 }
503 }
504 }