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