MavenVersionComparator.java
package network.ike.plugin.support.version;
import java.util.Comparator;
/**
* Comparator that orders Maven version strings the same way Maven 4's
* {@code DefaultArtifactVersion} does — broadly: split on
* {@code .}/{@code -}/{@code _}/digit-letter transitions, compare
* numeric segments numerically, alpha segments lexicographically with
* a few well-known qualifier ranks ({@code alpha < beta < milestone <
* rc < snapshot < (release) < sp}).
*
* <p>This is a pragmatic re-implementation, not a full clone of
* Maven's {@code ComparableVersion}. It handles the kinds of versions
* IKE actually publishes (single-segment integer like {@code "127"};
* dotted like {@code "1.59.0"}; SNAPSHOT-suffixed like
* {@code "128-SNAPSHOT"}) plus the common third-party patterns we
* regularly upgrade against (e.g., JUnit
* {@code "5.11.4"}, asciidoctorj {@code "3.0.1"}). Using a hand-rolled
* comparator avoids forcing every caller of
* {@link CandidateVersionResolver} to depend on {@code maven-artifact}.
*/
public final class MavenVersionComparator implements Comparator<String> {
/** Shared instance. */
public static final MavenVersionComparator INSTANCE =
new MavenVersionComparator();
private MavenVersionComparator() {}
@Override
public int compare(String a, String b) {
if (a == null && b == null) {
return 0;
}
if (a == null) {
return -1;
}
if (b == null) {
return 1;
}
String[] aSegs = split(a);
String[] bSegs = split(b);
int len = Math.max(aSegs.length, bSegs.length);
for (int i = 0; i < len; i++) {
String aSeg = i < aSegs.length ? aSegs[i] : "";
String bSeg = i < bSegs.length ? bSegs[i] : "";
int c = compareSegment(aSeg, bSeg);
if (c != 0) {
return c;
}
}
return 0;
}
/**
* Split a version string into comparable segments. Splits on
* {@code .}, {@code -}, and {@code _}; also inserts a split at
* each digit/letter transition so {@code "1rc1"} becomes
* {@code ["1","rc","1"]}.
*
* @param v version string
* @return segments in order
*/
static String[] split(String v) {
java.util.ArrayList<String> out = new java.util.ArrayList<>();
StringBuilder current = new StringBuilder();
boolean lastDigit = false;
for (int i = 0; i < v.length(); i++) {
char c = v.charAt(i);
if (c == '.' || c == '-' || c == '_') {
if (current.length() > 0) {
out.add(current.toString());
current.setLength(0);
}
lastDigit = false;
continue;
}
boolean isDigit = Character.isDigit(c);
if (current.length() > 0 && isDigit != lastDigit) {
out.add(current.toString());
current.setLength(0);
}
current.append(c);
lastDigit = isDigit;
}
if (current.length() > 0) {
out.add(current.toString());
}
return out.toArray(new String[0]);
}
private static int compareSegment(String a, String b) {
boolean aDigit = isAllDigits(a);
boolean bDigit = isAllDigits(b);
if (aDigit && bDigit) {
// Numeric segments compare numerically. Strip leading
// zeros to avoid Long overflow on absurd inputs; we don't
// expect huge segments anyway.
return compareNumeric(a, b);
}
if (aDigit) {
// Numeric beats alpha (1.0 > 1.0-alpha)
return 1;
}
if (bDigit) {
return -1;
}
return Integer.compare(qualifierRank(a), qualifierRank(b));
}
private static boolean isAllDigits(String s) {
if (s.isEmpty()) {
return false;
}
for (int i = 0; i < s.length(); i++) {
if (!Character.isDigit(s.charAt(i))) {
return false;
}
}
return true;
}
private static int compareNumeric(String a, String b) {
// Strip leading zeros; compare by length then char-by-char.
String an = a.replaceFirst("^0+(?!$)", "");
String bn = b.replaceFirst("^0+(?!$)", "");
if (an.length() != bn.length()) {
return Integer.compare(an.length(), bn.length());
}
return an.compareTo(bn);
}
/**
* Rank known qualifiers so {@code alpha < beta < milestone < rc <
* snapshot < (anything else, including release names) < sp}. This
* matches the practical ordering Maven uses for these qualifiers.
*
* @param qualifier qualifier string (case-insensitive)
* @return ordering rank (lower comes first)
*/
private static int qualifierRank(String qualifier) {
String q = qualifier.toLowerCase(java.util.Locale.ROOT);
return switch (q) {
case "a", "alpha" -> 1;
case "b", "beta" -> 2;
case "m", "milestone" -> 3;
case "rc", "cr" -> 4;
case "snapshot" -> 5;
case "ga", "final", "release", "" -> 6;
case "sp" -> 7;
default -> 6; // unknown alpha tags compare equal to release
};
}
}