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