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 */ 019package org.eclipse.aether.util.version; 020 021import java.math.BigInteger; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.List; 025import java.util.Locale; 026 027import org.eclipse.aether.version.Version; 028 029import static java.util.Objects.requireNonNull; 030 031/** 032 * A generic version, that is a version that accepts any input string and tries to apply common sense sorting. See 033 * {@link GenericVersionScheme} for details. 034 */ 035public final class GenericVersion implements Version { 036 037 private final String version; 038 039 private final List<Item> items; 040 041 private final int hash; 042 043 /** 044 * Creates a generic version from the specified string. 045 * 046 * @param version the version string, must not be {@code null} 047 */ 048 GenericVersion(String version) { 049 this.version = requireNonNull(version, "version cannot be null"); 050 items = parse(version); 051 hash = items.hashCode(); 052 } 053 054 /** 055 * Returns this instance backing string representation. 056 * 057 * @since 1.9.5 058 */ 059 public String asString() { 060 return version; 061 } 062 063 /** 064 * Returns this instance tokenized representation as unmodifiable list. 065 * 066 * @since 1.9.5 067 */ 068 public List<Item> asItems() { 069 return items; 070 } 071 072 private static List<Item> parse(String version) { 073 List<Item> items = new ArrayList<>(); 074 075 for (Tokenizer tokenizer = new Tokenizer(version); tokenizer.next(); ) { 076 Item item = tokenizer.toItem(); 077 items.add(item); 078 } 079 080 trimPadding(items); 081 082 return Collections.unmodifiableList(items); 083 } 084 085 /** 086 * Visible for testing. 087 */ 088 static void trimPadding(List<Item> items) { 089 Boolean number = null; 090 int end = items.size() - 1; 091 for (int i = end; i > 0; i--) { 092 Item item = items.get(i); 093 if (!Boolean.valueOf(item.isNumber()).equals(number)) { 094 end = i; 095 number = item.isNumber(); 096 } 097 if (end == i 098 && (i == items.size() - 1 || items.get(i - 1).isNumber() == item.isNumber()) 099 && item.compareTo(null) == 0) { 100 items.remove(i); 101 end--; 102 } 103 } 104 } 105 106 @Override 107 public int compareTo(Version obj) { 108 final List<Item> these = items; 109 final List<Item> those = ((GenericVersion) obj).items; 110 111 boolean number = true; 112 113 for (int index = 0; ; index++) { 114 if (index >= these.size() && index >= those.size()) { 115 return 0; 116 } else if (index >= these.size()) { 117 return -comparePadding(those, index, null); 118 } else if (index >= those.size()) { 119 return comparePadding(these, index, null); 120 } 121 122 Item thisItem = these.get(index); 123 Item thatItem = those.get(index); 124 125 if (thisItem.isNumber() != thatItem.isNumber()) { 126 if (index == 0) { 127 return thisItem.compareTo(thatItem); 128 } 129 if (number == thisItem.isNumber()) { 130 return comparePadding(these, index, number); 131 } else { 132 return -comparePadding(those, index, number); 133 } 134 } else { 135 int rel = thisItem.compareTo(thatItem); 136 if (rel != 0) { 137 return rel; 138 } 139 number = thisItem.isNumber(); 140 } 141 } 142 } 143 144 private static int comparePadding(List<Item> items, int index, Boolean number) { 145 int rel = 0; 146 for (int i = index; i < items.size(); i++) { 147 Item item = items.get(i); 148 if (number != null && number != item.isNumber()) { 149 // do not stop here, but continue, skipping non-number members 150 continue; 151 } 152 rel = item.compareTo(null); 153 if (rel != 0) { 154 break; 155 } 156 } 157 return rel; 158 } 159 160 @Override 161 public boolean equals(Object obj) { 162 return (obj instanceof GenericVersion) && compareTo((GenericVersion) obj) == 0; 163 } 164 165 @Override 166 public int hashCode() { 167 return hash; 168 } 169 170 @Override 171 public String toString() { 172 return version; 173 } 174 175 static final class Tokenizer { 176 private final String version; 177 178 private final int versionLength; 179 180 private int index; 181 182 private String token; 183 184 private boolean number; 185 186 private boolean terminatedByNumber; 187 188 Tokenizer(String version) { 189 this.version = (!version.isEmpty()) ? version : "0"; 190 this.versionLength = this.version.length(); 191 } 192 193 public boolean next() { 194 if (index >= versionLength) { 195 return false; 196 } 197 198 int state = -2; 199 200 int start = index; 201 int end = versionLength; 202 terminatedByNumber = false; 203 204 for (; index < versionLength; index++) { 205 char c = version.charAt(index); 206 207 if (c == '.' || c == '-' || c == '_') { 208 end = index; 209 index++; 210 break; 211 } else { 212 if (c >= '0' && c <= '9') { // only ASCII digits 213 int digit = c - '0'; 214 if (state == -1) { 215 end = index; 216 terminatedByNumber = true; 217 break; 218 } 219 if (state == 0) { 220 // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling) 221 start++; 222 } 223 state = (state > 0 || digit > 0) ? 1 : 0; 224 } else { 225 if (state >= 0) { 226 end = index; 227 break; 228 } 229 state = -1; 230 } 231 } 232 } 233 234 if (end - start > 0) { 235 token = version.substring(start, end); 236 number = state >= 0; 237 } else { 238 token = "0"; 239 number = true; 240 } 241 242 return true; 243 } 244 245 @Override 246 public String toString() { 247 return String.valueOf(token); 248 } 249 250 public Item toItem() { 251 if (number) { 252 try { 253 if (token.length() < 10) { 254 return new Item(Item.KIND_INT, Integer.parseInt(token)); 255 } else { 256 return new Item(Item.KIND_BIGINT, new BigInteger(token)); 257 } 258 } catch (NumberFormatException e) { 259 throw new IllegalStateException(e); 260 } 261 } else { 262 if (index >= version.length()) { 263 if ("min".equalsIgnoreCase(token)) { 264 return Item.MIN; 265 } else if ("max".equalsIgnoreCase(token)) { 266 return Item.MAX; 267 } 268 } 269 if (terminatedByNumber && token.length() == 1) { 270 switch (token.charAt(0)) { 271 case 'a': 272 case 'A': 273 token = GenericQualifiers.LABEL_ALPHA; 274 break; 275 case 'b': 276 case 'B': 277 token = GenericQualifiers.LABEL_BETA; 278 break; 279 case 'm': 280 case 'M': 281 token = GenericQualifiers.LABEL_MILESTONE; 282 break; 283 default: 284 } 285 } 286 token = token.toLowerCase(Locale.ENGLISH); 287 return GenericQualifiers.tokenQualifier(token) 288 .map(integer -> new Item(Item.KIND_QUALIFIER, integer)) 289 .orElseGet(() -> new Item(Item.KIND_STRING, token)); 290 } 291 } 292 } 293 294 static final class Item { 295 296 static final int KIND_MAX = 8; 297 298 static final int KIND_BIGINT = 5; 299 300 static final int KIND_INT = 4; 301 302 static final int KIND_STRING = 3; 303 304 static final int KIND_QUALIFIER = 2; 305 306 static final int KIND_MIN = 0; 307 308 static final Item MAX = new Item(KIND_MAX, "max"); 309 310 static final Item MIN = new Item(KIND_MIN, "min"); 311 312 private final int kind; 313 314 private final Object value; 315 316 Item(int kind, Object value) { 317 this.kind = kind; 318 this.value = value; 319 } 320 321 public boolean isNumber() { 322 return (kind & KIND_QUALIFIER) == 0; // i.e. kind != string/qualifier 323 } 324 325 public int compareTo(Item that) { 326 int rel; 327 if (that == null) { 328 // null in this context denotes the pad item (0 or "ga") 329 switch (kind) { 330 case KIND_MIN: 331 rel = -1; 332 break; 333 case KIND_MAX: 334 case KIND_BIGINT: 335 case KIND_STRING: 336 rel = 1; 337 break; 338 case KIND_INT: 339 case KIND_QUALIFIER: 340 rel = (Integer) value; 341 break; 342 default: 343 throw new IllegalStateException("unknown version item kind " + kind); 344 } 345 } else { 346 rel = kind - that.kind; 347 if (rel == 0) { 348 switch (kind) { 349 case KIND_MAX: 350 case KIND_MIN: 351 break; 352 case KIND_BIGINT: 353 rel = ((BigInteger) value).compareTo((BigInteger) that.value); 354 break; 355 case KIND_INT: 356 case KIND_QUALIFIER: 357 rel = ((Integer) value).compareTo((Integer) that.value); 358 break; 359 case KIND_STRING: 360 rel = ((String) value).compareToIgnoreCase((String) that.value); 361 break; 362 default: 363 throw new IllegalStateException("unknown version item kind " + kind); 364 } 365 } 366 } 367 return rel; 368 } 369 370 @Override 371 public boolean equals(Object obj) { 372 return (obj instanceof Item) && compareTo((Item) obj) == 0; 373 } 374 375 @Override 376 public int hashCode() { 377 return value.hashCode() + kind * 31; 378 } 379 380 @Override 381 public String toString() { 382 return String.valueOf(value); 383 } 384 } 385}