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}