Skip to content

Commit 28feee8

Browse files
committed
[MNG-7559] Fix version comparison with case insensitive lexical order
1 parent a336a2c commit 28feee8

File tree

3 files changed

+97
-58
lines changed

3 files changed

+97
-58
lines changed

compat/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@
2323
import java.util.ArrayList;
2424
import java.util.Arrays;
2525
import java.util.Deque;
26+
import java.util.HashMap;
2627
import java.util.Iterator;
2728
import java.util.List;
2829
import java.util.Locale;
30+
import java.util.Map;
2931
import java.util.Objects;
30-
import java.util.Properties;
3132

3233
/**
3334
* <p>
@@ -41,20 +42,21 @@
4142
* <code>1.0alpha1 =&gt; [1, [alpha, 1]]</code></li>
4243
* <li>Unlimited number of version components,</li>
4344
* <li>Version components in the text can be digits or strings,</li>
44-
* <li>Strings are checked for well-known qualifiers, and the qualifier ordering is used for version ordering.
45-
* Well-known qualifiers (case-insensitive) are, in order from least to greatest:<ol>
46-
* <li><code>alpha</code> or <code>a</code></li>
47-
* <li><code>beta</code> or <code>b</code></li>
48-
* <li><code>milestone</code> or <code>m</code></li>
49-
* <li><code>rc</code> or <code>cr</code></li>
50-
* <li><code>snapshot</code></li>
51-
* <li><code>ga</code> or <code>final</code></li>
52-
* <li><code>sp</code></li>
53-
* </ol>
54-
* Unknown qualifiers are considered after known qualifiers,
55-
* with lexical order (case-insensitive in the English locale).
56-
* <code>ga</code> and <code>final</code> sort the same as not having a qualifier.
57-
* </li>
45+
* <li>
46+
* Following semver rules is encouraged, and some qualifiers are discouraged (no matter the case sensitivity):
47+
* <ul>
48+
* <li> The usage of 'CR' qualifier is discouraged. Use 'RC' instead. </li>
49+
* <li> The usage of 'Final', 'GA', and 'Release' qualifiers is discouraged. Use no qualifier instead. </li>
50+
* <li> The usage of 'SP' qualifier is discouraged. Increment the patch version instead. </li>
51+
* </ul>
52+
* String qualifiers are ordered lexically (case insensitive in the English locale), with the following exceptions:
53+
* <ul>
54+
* <li><code> alpha = a &lt; beta = b &lt; milestone = m &lt; rc = cr
55+
* &lt; snapshot &lt; '' = final = ga = release &lt; sp </code></li>
56+
* <li>Other qualifiers are ordered lexically (case insensitive in the English locale),
57+
* and considered less than Snapshot and Release.</li>
58+
* </ul>
59+
* </li>
5860
* <li>A hyphen usually precedes a qualifier, and is always less important than digits/number. For example
5961
* {@code 1.0.RC2 < 1.0-RC3 < 1.0.1}; but prefer {@code 1.0.0-RC2} over {@code 1.0.0.RC2}, and more
6062
* generally: {@code 1.0.X2 < 1.0-X3 < 1.0.1} for any string {@code X}; but prefer {@code 1.0.0-X1}
@@ -295,41 +297,37 @@ public String toString() {
295297
* Represents a string in the version item list, usually a qualifier.
296298
*/
297299
private static class StringItem implements Item {
298-
private static final List<String> QUALIFIERS =
299-
Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp");
300-
private static final List<String> RELEASE_QUALIFIERS = Arrays.asList("ga", "final", "release");
300+
private static final List<String> QUALIFIERS = Arrays.asList("snapshot", "", "sp");
301+
private static final List<String> RELEASE_QUALIFIERS = Arrays.asList("final", "ga", "release");
301302

302-
private static final Properties ALIASES = new Properties();
303+
private static final Map<String, String> ALIASES = new HashMap<>(4);
303304

304305
static {
305306
ALIASES.put("cr", "rc");
307+
ALIASES.put("final", "");
308+
ALIASES.put("ga", "");
309+
ALIASES.put("release", "");
306310
}
307311

308312
/**
309-
* A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
313+
* An index value for the empty-string qualifier. This one is used to determine if a given qualifier makes
310314
* the version older than one without a qualifier, or more recent.
311315
*/
312-
private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf(""));
316+
private static final int RELEASE_VERSION_INDEX = QUALIFIERS.indexOf("");
313317

314318
private final String value;
315319

316320
StringItem(String value, boolean followedByDigit) {
317321
if (followedByDigit && value.length() == 1) {
318322
// a1 = alpha-1, b1 = beta-1, m1 = milestone-1
319-
switch (value.charAt(0)) {
320-
case 'a':
321-
value = "alpha";
322-
break;
323-
case 'b':
324-
value = "beta";
325-
break;
326-
case 'm':
327-
value = "milestone";
328-
break;
329-
default:
330-
}
323+
value = switch (value.charAt(0)) {
324+
case 'a' -> "alpha";
325+
case 'b' -> "beta";
326+
case 'm' -> "milestone";
327+
default -> value;
328+
};
331329
}
332-
this.value = ALIASES.getProperty(value, value);
330+
this.value = ALIASES.getOrDefault(value, value);
333331
}
334332

335333
@Override
@@ -351,27 +349,55 @@ public boolean isNull() {
351349
*
352350
* @param qualifier
353351
* @return an equivalent value that can be used with lexical comparison
352+
* @deprecated Use {@link #compareQualifiers(String, String)} instead
354353
*/
354+
@Deprecated
355355
public static String comparableQualifier(String qualifier) {
356+
// Just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for
357+
// -1 or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first
358+
// character, so this is still fast. If more characters are needed then it requires a lexical sort anyway.
359+
356360
if (RELEASE_QUALIFIERS.contains(qualifier)) {
357-
return String.valueOf(QUALIFIERS.indexOf(""));
361+
qualifier = "";
358362
}
359363

360-
int i = QUALIFIERS.indexOf(qualifier);
364+
int index = QUALIFIERS.indexOf(qualifier) + 1;
361365

362-
// Just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for
363-
// -1
364-
// or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first
365-
// character,
366-
// so this is still fast. If more characters are needed then it requires a lexical sort anyway.
367-
return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i);
366+
return index == 0 ? ("0-" + qualifier) : String.valueOf(index);
367+
}
368+
369+
/**
370+
* Compare the qualifiers of two artifact versions.
371+
*
372+
* @param qualifier1 qualifier of first artifact
373+
* @param qualifier2 qualifier of second artifact
374+
* @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or
375+
* greater than the second
376+
*/
377+
public static int compareQualifiers(String qualifier1, String qualifier2) {
378+
if (RELEASE_QUALIFIERS.contains(qualifier1)) {
379+
qualifier1 = "";
380+
}
381+
if (RELEASE_QUALIFIERS.contains(qualifier2)) {
382+
qualifier2 = "";
383+
}
384+
int i1 = QUALIFIERS.indexOf(qualifier1);
385+
int i2 = QUALIFIERS.indexOf(qualifier2);
386+
387+
// if both pre-release, then use natural lexical ordering
388+
if (i1 == -1 && i2 == -1) {
389+
return qualifier1.compareTo(qualifier2);
390+
}
391+
392+
// 'other qualifier' < 'snapshot' < '' < 'sp'
393+
return Integer.compare(i1, i2);
368394
}
369395

370396
@Override
371397
public int compareTo(Item item) {
372398
if (item == null) {
373399
// 1-rc < 1, 1-ga > 1
374-
return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
400+
return Integer.compare(QUALIFIERS.indexOf(value), RELEASE_VERSION_INDEX);
375401
}
376402
switch (item.getType()) {
377403
case INT_ITEM:
@@ -380,7 +406,7 @@ public int compareTo(Item item) {
380406
return -1; // 1.any < 1.1 ?
381407

382408
case STRING_ITEM:
383-
return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value));
409+
return compareQualifiers(value, ((StringItem) item).value);
384410

385411
case COMBINATION_ITEM:
386412
int result = this.compareTo(((CombinationItem) item).getStringPart());

compat/maven-artifact/src/test/java/org/apache/maven/artifact/versioning/ComparableVersionTest.java

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,16 @@ private ComparableVersion newComparable(String version) {
4444
}
4545

4646
private static final String[] VERSIONS_QUALIFIER = {
47+
"1-abc",
4748
"1-alpha2snapshot",
4849
"1-alpha2",
4950
"1-alpha-123",
5051
"1-beta-2",
5152
"1-beta123",
53+
"1-def",
5254
"1-m2",
5355
"1-m11",
56+
"1-pom-1",
5457
"1-rc",
5558
"1-cr2",
5659
"1-rc123",
@@ -59,18 +62,15 @@ private ComparableVersion newComparable(String version) {
5962
"1-sp",
6063
"1-sp2",
6164
"1-sp123",
62-
"1-abc",
63-
"1-def",
64-
"1-pom-1",
6565
"1-1-snapshot",
6666
"1-1",
6767
"1-2",
6868
"1-123"
6969
};
7070

7171
private static final String[] VERSIONS_NUMBER = {
72-
"2.0", "2.0.a", "2-1", "2.0.2", "2.0.123", "2.1.0", "2.1-a", "2.1b", "2.1-c", "2.1-1", "2.1.0.1", "2.2",
73-
"2.123", "11.a2", "11.a11", "11.b2", "11.b11", "11.m2", "11.m11", "11", "11.a", "11b", "11c", "11m"
72+
"2.0.a", "2.0", "2-1", "2.0.2", "2.0.123", "2.1-a", "2.1b", "2.1-c", "2.1.0", "2.1-1", "2.1.0.1", "2.2",
73+
"2.123", "11.a", "11.a2", "11.a11", "11b", "11.b2", "11.b11", "11c", "11m", "11.m2", "11.m11", "11"
7474
};
7575

7676
private void checkVersionsOrder(String[] versions) {
@@ -212,7 +212,7 @@ void testVersionComparing() {
212212

213213
checkVersionsOrder("2.0-1", "2.0.1");
214214
checkVersionsOrder("2.0.1-klm", "2.0.1-lmn");
215-
checkVersionsOrder("2.0.1", "2.0.1-xyz");
215+
checkVersionsOrder("2.0.1-xyz", "2.0.1"); // now 2.0.1-xyz < 2.0.1 as of MNG-7559
216216

217217
checkVersionsOrder("2.0.1", "2.0.1-123");
218218
checkVersionsOrder("2.0.1-xyz", "2.0.1-123");
@@ -328,13 +328,9 @@ void testLexicographicOrder() {
328328
*/
329329
@Test
330330
void testMng5568() {
331-
String a = "6.1.0";
332-
String b = "6.1.0rc3";
333-
String c = "6.1H.5-beta"; // this is the unusual version string, with 'H' in the middle
334-
335-
checkVersionsOrder(b, a); // classical
336-
checkVersionsOrder(b, c); // now b < c, but before MNG-5568, we had b > c
337-
checkVersionsOrder(a, c);
331+
checkVersionsOrder("6.1H.5-beta", "6.1.0rc3"); // now H < RC as of MNG-7559
332+
checkVersionsOrder("6.1.0rc3", "6.1.0"); // classical
333+
checkVersionsOrder("6.1H.5-beta", "6.1.0"); // transitivity
338334
}
339335

340336
/**
@@ -458,6 +454,23 @@ void testReuse() {
458454
assertEquals(c1, c2, "reused instance should be equivalent to new instance");
459455
}
460456

457+
/**
458+
* Test <a href="https://issues.apache.org/jira/browse/MNG-7559">MNG-7559</a> edge cases
459+
* -pfd < final, ga, release
460+
* 2.0.1.MR < 2.0.1
461+
* 9.4.1.jre16 > 9.4.1.jre16-preview
462+
*/
463+
@Test
464+
void testMng7559() {
465+
// checking general cases
466+
checkVersionsOrder(new String[] {"ab", "alpha", "beta", "cd", "ea", "milestone", "mr", "pfd", "preview", "RC"});
467+
// checking identified issues respect the general case
468+
checkVersionsOrder("2.3-pfd", "2.3");
469+
checkVersionsOrder("2.0.1.MR", "2.0.1");
470+
checkVersionsOrder("9.4.1.jre16-preview", "9.4.1.jre16");
471+
checkVersionsOrder("1-ga-1", "1-sp-1");
472+
}
473+
461474
/**
462475
* Test <a href="https://issues.apache.org/jira/browse/MNG-7644">MNG-7644</a> edge cases
463476
* 1.0.0.RC1 &lt; 1.0.0-RC2 and more generally:

compat/maven-artifact/src/test/java/org/apache/maven/artifact/versioning/DefaultArtifactVersionTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ void testVersionComparing() {
118118
assertVersionOlder("2.0-1", "2.0.1");
119119

120120
assertVersionOlder("2.0.1-klm", "2.0.1-lmn");
121-
assertVersionOlder("2.0.1", "2.0.1-xyz");
121+
assertVersionOlder("2.0.1-xyz", "2.0.1"); // now 2.0.1-xyz < 2.0.1 as of MNG-7559
122122
assertVersionOlder("2.0.1-xyz-1", "2.0.1-1-xyz");
123123

124124
assertVersionOlder("2.0.1", "2.0.1-123");

0 commit comments

Comments
 (0)