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