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 =&gt; [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    }