001    package org.apache.archiva.common.utils;
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 org.apache.commons.lang.ArrayUtils;
023    import org.apache.commons.lang.StringUtils;
024    import org.apache.commons.lang.math.NumberUtils;
025    
026    import java.util.ArrayList;
027    import java.util.Comparator;
028    import java.util.List;
029    
030    /**
031     * VersionComparator - compare the parts of two version strings.
032     * <p/>
033     * Technique.
034     * <p/>
035     * * Split the version strings into parts by splitting on <code>"-._"</code> first, then breaking apart words from numbers.
036     * <p/>
037     * <code>
038     * "1.0"         = "1", "0"
039     * "1.0-alpha-1" = "1", "0", "alpha", "1"
040     * "2.0-rc2"     = "2", "0", "rc", "2"
041     * "1.3-m2"      = "1", "3", "m", "3"
042     * </code>
043     * <p/>
044     * compare each part individually, and when they do not match, perform the following test.
045     * <p/>
046     * Numbers are calculated per normal comparison rules.
047     * Words that are part of the "special word list" will be treated as their index within that heirarchy.
048     * Words that cannot be identified as special, are treated using normal case-insensitive comparison rules.
049     *
050     *
051     */
052    public class VersionComparator
053        implements Comparator<String>
054    {
055        private static Comparator<String> INSTANCE = new VersionComparator();
056    
057        private List<String> specialWords;
058    
059        public VersionComparator()
060        {
061            specialWords = new ArrayList<String>( 23 );
062    
063            // ids that refer to LATEST
064            specialWords.add( "final" );
065            specialWords.add( "release" );
066            specialWords.add( "current" );
067            specialWords.add( "latest" );
068            specialWords.add( "g" );
069            specialWords.add( "gold" );
070            specialWords.add( "fcs" );
071    
072            // ids that are for a release cycle.
073            specialWords.add( "a" );
074            specialWords.add( "alpha" );
075            specialWords.add( "b" );
076            specialWords.add( "beta" );
077            specialWords.add( "pre" );
078            specialWords.add( "rc" );
079            specialWords.add( "m" );
080            specialWords.add( "milestone" );
081    
082            // ids that are for dev / debug cycles.
083            specialWords.add( "dev" );
084            specialWords.add( "test" );
085            specialWords.add( "debug" );
086            specialWords.add( "unofficial" );
087            specialWords.add( "nightly" );
088            specialWords.add( "incubating" );
089            specialWords.add( "incubator" );
090            specialWords.add( "snapshot" );
091        }
092    
093        public static Comparator<String> getInstance()
094        {
095            return INSTANCE;
096        }
097    
098        public int compare( String o1, String o2 )
099        {
100            if ( o1 == null && o2 == null )
101            {
102                return 0;
103            }
104    
105            if ( o1 == null )
106            {
107                return 1;
108            }
109    
110            if ( o2 == null )
111            {
112                return -1;
113            }
114    
115            String[] parts1 = toParts( o1 );
116            String[] parts2 = toParts( o2 );
117    
118            int diff;
119            int partLen = Math.max( parts1.length, parts2.length );
120            for ( int i = 0; i < partLen; i++ )
121            {
122                diff = comparePart( safePart( parts1, i ), safePart( parts2, i ) );
123                if ( diff != 0 )
124                {
125                    return diff;
126                }
127            }
128    
129            diff = parts2.length - parts1.length;
130    
131            if ( diff != 0 )
132            {
133                return diff;
134            }
135    
136            return o1.compareToIgnoreCase( o2 );
137        }
138    
139        private String safePart( String[] parts, int idx )
140        {
141            if ( idx < parts.length )
142            {
143                return parts[idx];
144            }
145    
146            return "0";
147        }
148    
149        private int comparePart( String s1, String s2 )
150        {
151            boolean is1Num = NumberUtils.isNumber( s1 );
152            boolean is2Num = NumberUtils.isNumber( s2 );
153    
154            // (Special Case) Test for numbers both first.
155            if ( is1Num && is2Num )
156            {
157                int i1 = NumberUtils.toInt( s1 );
158                int i2 = NumberUtils.toInt( s2 );
159    
160                return i1 - i2;
161            }
162    
163            // Test for text both next.
164            if ( !is1Num && !is2Num )
165            {
166                int idx1 = specialWords.indexOf( s1.toLowerCase() );
167                int idx2 = specialWords.indexOf( s2.toLowerCase() );
168    
169                // Only operate perform index based operation, if both strings
170                // are found in the specialWords index.
171                if ( idx1 >= 0 && idx2 >= 0 )
172                {
173                    return idx1 - idx2;
174                }
175            }
176    
177            // Comparing text to num
178            if ( !is1Num && is2Num )
179            {
180                return -1;
181            }
182    
183            // Comparing num to text
184            if ( is1Num && !is2Num )
185            {
186                return 1;
187            }
188    
189            // Return comparison of strings themselves.
190            return s1.compareToIgnoreCase( s2 );
191        }
192    
193        public static String[] toParts( String version )
194        {
195            if ( StringUtils.isBlank( version ) )
196            {
197                return ArrayUtils.EMPTY_STRING_ARRAY;
198            }
199    
200            int modeOther = 0;
201            int modeDigit = 1;
202            int modeText = 2;
203    
204            List<String> parts = new ArrayList<String>();
205            int len = version.length();
206            int i = 0;
207            int start = 0;
208            int mode = modeOther;
209    
210            while ( i < len )
211            {
212                char c = version.charAt( i );
213    
214                if ( Character.isDigit( c ) )
215                {
216                    if ( mode != modeDigit )
217                    {
218                        if ( mode != modeOther )
219                        {
220                            parts.add( version.substring( start, i ) );
221                        }
222                        mode = modeDigit;
223                        start = i;
224                    }
225                }
226                else if ( Character.isLetter( c ) )
227                {
228                    if ( mode != modeText )
229                    {
230                        if ( mode != modeOther )
231                        {
232                            parts.add( version.substring( start, i ) );
233                        }
234                        mode = modeText;
235                        start = i;
236                    }
237                }
238                else
239                {
240                    // Other.
241                    if ( mode != modeOther )
242                    {
243                        parts.add( version.substring( start, i ) );
244                        mode = modeOther;
245                    }
246                }
247    
248                i++;
249            }
250    
251            // Add remainder
252            if ( mode != modeOther )
253            {
254                parts.add( version.substring( start, i ) );
255            }
256    
257            return parts.toArray( new String[parts.size()] );
258        }
259    }