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