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