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//
015package 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
036import java.math.BigInteger;
037import java.util.ArrayList;
038import java.util.Arrays;
039import java.util.Iterator;
040import java.util.List;
041import java.util.ListIterator;
042import java.util.Locale;
043import java.util.Properties;
044import 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 =&gt; [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 */
073public 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}