Skip to content

Commit ed7fb4d

Browse files
committed
[MNG-7559] Fix version comparison with case insensitive lexical order
1 parent 35c81be commit ed7fb4d

File tree

3 files changed

+80
-41
lines changed

3 files changed

+80
-41
lines changed

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

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@
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;
29-
import java.util.Properties;
30+
import java.util.Map;
3031

3132
/**
3233
* <p>
@@ -40,25 +41,28 @@
4041
* <code>1.0alpha1 =&gt; [1, 0, alpha, 1]</code></li>
4142
* <li>unlimited number of version components,</li>
4243
* <li>version components in the text can be digits or strings,</li>
43-
* <li>strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
44-
* Well-known qualifiers (case insensitive) are:<ul>
45-
* <li><code>alpha</code> or <code>a</code></li>
46-
* <li><code>beta</code> or <code>b</code></li>
47-
* <li><code>milestone</code> or <code>m</code></li>
48-
* <li><code>rc</code> or <code>cr</code></li>
49-
* <li><code>snapshot</code></li>
50-
* <li><code>(the empty string)</code> or <code>ga</code> or <code>final</code></li>
51-
* <li><code>sp</code></li>
52-
* </ul>
53-
* Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive),
54-
* </li>
44+
* <li>
45+
* Following semver rules is encouraged, and some qualifiers are discouraged (no matter the case):
46+
* <ul>
47+
* <li> The usage of 'CR' qualifier is discouraged. Use 'RC' instead. </li>
48+
* <li> The usage of 'Final', 'GA', and 'Release' qualifiers is discouraged. Use no qualifier instead. </li>
49+
* <li> The usage of 'SP' qualifier is discouraged. Increment the patch version instead. </li>
50+
* </ul>
51+
* String qualifiers are ordered lexically (case insensitive), with the following exceptions:
52+
* <ul>
53+
* <li><code> alpha = a &lt; beta = b &lt; milestone = m &lt; rc = cr
54+
* &lt; snapshot &lt; '' = final = ga = release &lt; sp </code></li>
55+
* <li>Other qualifiers are ordered lexically (case insensitive),
56+
* and considered less than Snapshot and Release.</li>
57+
* </ul>
58+
* </li>
5559
* <li>a hyphen usually precedes a qualifier, and is always less important than digits/number, for example
5660
* {@code 1.0.RC2 < 1.0-RC3 < 1.0.1}; but prefer {@code 1.0.0-RC1} over {@code 1.0.0.RC1}, and more
5761
* generally: {@code 1.0.X2 < 1.0-X3 < 1.0.1} for any string {@code X}; but prefer {@code 1.0.0-X1}
5862
* over {@code 1.0.0.X1}.</li>
5963
* </ul>
6064
*
61-
* @see <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning" on Maven Wiki</a>
65+
* @see <a href="https://maven.apache.org/pom.html#Version_Order_Specification">Version Order Specification</a>
6266
* @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
6367
* @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
6468
*/
@@ -307,23 +311,22 @@ public String toString() {
307311
* Represents a string in the version item list, usually a qualifier.
308312
*/
309313
private static class StringItem implements Item {
310-
private static final List<String> QUALIFIERS =
311-
Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp");
314+
private static final List<String> QUALIFIERS = Arrays.asList("snapshot", "", "sp");
312315

313-
private static final Properties ALIASES = new Properties();
316+
private static final Map<String, String> ALIASES = new HashMap<>(4);
314317

315318
static {
316-
ALIASES.put("ga", "");
319+
ALIASES.put("cr", "rc");
317320
ALIASES.put("final", "");
321+
ALIASES.put("ga", "");
318322
ALIASES.put("release", "");
319-
ALIASES.put("cr", "rc");
320323
}
321324

322325
/**
323-
* A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
326+
* An index value for the empty-string qualifier. This one is used to determine if a given qualifier makes
324327
* the version older than one without a qualifier, or more recent.
325328
*/
326-
private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf(""));
329+
private static final int RELEASE_VERSION_INDEX = QUALIFIERS.indexOf("");
327330

328331
private final String value;
329332

@@ -343,7 +346,7 @@ private static class StringItem implements Item {
343346
default:
344347
}
345348
}
346-
this.value = ALIASES.getProperty(value, value);
349+
this.value = ALIASES.getOrDefault(value, value);
347350
}
348351

349352
@Override
@@ -353,7 +356,7 @@ public int getType() {
353356

354357
@Override
355358
public boolean isNull() {
356-
return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0);
359+
return QUALIFIERS.indexOf(value) == RELEASE_VERSION_INDEX;
357360
}
358361

359362
/**
@@ -368,18 +371,41 @@ public boolean isNull() {
368371
*
369372
* @param qualifier
370373
* @return an equivalent value that can be used with lexical comparison
374+
* @deprecated Use {@link #compareQualifiers(String, String)} instead
371375
*/
376+
@Deprecated
372377
public static String comparableQualifier(String qualifier) {
373-
int i = QUALIFIERS.indexOf(qualifier);
378+
int index = QUALIFIERS.indexOf(qualifier) + 1;
379+
380+
return index == 0 ? ("0-" + qualifier) : String.valueOf(index);
381+
}
382+
383+
/**
384+
* Compare the qualifiers of two artifact versions.
385+
*
386+
* @param qualifier1 qualifier of first artifact
387+
* @param qualifier2 qualifier of second artifact
388+
* @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or
389+
* greater than the second
390+
*/
391+
public static int compareQualifiers(String qualifier1, String qualifier2) {
392+
int i1 = QUALIFIERS.indexOf(qualifier1);
393+
int i2 = QUALIFIERS.indexOf(qualifier2);
394+
395+
// if both pre-release, then use natural lexical ordering
396+
if (i1 == -1 && i2 == -1) {
397+
return qualifier1.compareTo(qualifier2);
398+
}
374399

375-
return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i);
400+
// 'other qualifier' < 'snapshot' < '' < 'sp'
401+
return Integer.compare(i1, i2);
376402
}
377403

378404
@Override
379405
public int compareTo(Item item) {
380406
if (item == null) {
381407
// 1-rc < 1, 1-ga > 1
382-
return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
408+
return Integer.compare(QUALIFIERS.indexOf(value), RELEASE_VERSION_INDEX);
383409
}
384410
switch (item.getType()) {
385411
case INT_ITEM:
@@ -388,7 +414,7 @@ public int compareTo(Item item) {
388414
return -1; // 1.any < 1.1 ?
389415

390416
case STRING_ITEM:
391-
return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value));
417+
return compareQualifiers(value, ((StringItem) item).value);
392418

393419
case LIST_ITEM:
394420
return -1; // 1.any < 1-1

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
@@ -46,13 +46,16 @@ private Comparable newComparable(String version) {
4646
}
4747

4848
private static final String[] VERSIONS_QUALIFIER = {
49+
"1-abc",
4950
"1-alpha2snapshot",
5051
"1-alpha2",
5152
"1-alpha-123",
5253
"1-beta-2",
5354
"1-beta123",
55+
"1-def",
5456
"1-m2",
5557
"1-m11",
58+
"1-pom-1",
5659
"1-rc",
5760
"1-cr2",
5861
"1-rc123",
@@ -61,18 +64,15 @@ private Comparable newComparable(String version) {
6164
"1-sp",
6265
"1-sp2",
6366
"1-sp123",
64-
"1-abc",
65-
"1-def",
66-
"1-pom-1",
6767
"1-1-snapshot",
6868
"1-1",
6969
"1-2",
7070
"1-123"
7171
};
7272

7373
private static final String[] VERSIONS_NUMBER = {
74-
"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",
75-
"2.123", "11.a2", "11.a11", "11.b2", "11.b11", "11.m2", "11.m11", "11", "11.a", "11b", "11c", "11m"
74+
"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",
75+
"2.123", "11.a", "11.a2", "11.a11", "11b", "11.b2", "11.b11", "11c", "11m", "11.m2", "11.m11", "11"
7676
};
7777

7878
private void checkVersionsOrder(String[] versions) {
@@ -202,7 +202,7 @@ public void testVersionComparing() {
202202

203203
checkVersionsOrder("2.0-1", "2.0.1");
204204
checkVersionsOrder("2.0.1-klm", "2.0.1-lmn");
205-
checkVersionsOrder("2.0.1", "2.0.1-xyz");
205+
checkVersionsOrder("2.0.1-xyz", "2.0.1"); // now 2.0.1-xyz < 2.0.1 as of MNG-7559
206206

207207
checkVersionsOrder("2.0.1", "2.0.1-123");
208208
checkVersionsOrder("2.0.1-xyz", "2.0.1-123");
@@ -217,13 +217,9 @@ public void testVersionComparing() {
217217
*/
218218
@Test
219219
public void testMng5568() {
220-
String a = "6.1.0";
221-
String b = "6.1.0rc3";
222-
String c = "6.1H.5-beta"; // this is the unusual version string, with 'H' in the middle
223-
224-
checkVersionsOrder(b, a); // classical
225-
checkVersionsOrder(b, c); // now b < c, but before MNG-5568, we had b > c
226-
checkVersionsOrder(a, c);
220+
checkVersionsOrder("6.1H.5-beta", "6.1.0rc3"); // now H < RC as of MNG-7559
221+
checkVersionsOrder("6.1.0rc3", "6.1.0"); // classical
222+
checkVersionsOrder("6.1H.5-beta", "6.1.0"); // transitivity
227223
}
228224

229225
/**
@@ -347,6 +343,23 @@ public void testReuse() {
347343
assertEquals(c1, c2, "reused instance should be equivalent to new instance");
348344
}
349345

346+
/**
347+
* Test <a href="https://issues.apache.org/jira/browse/MNG-7559">MNG-7559</a> edge cases
348+
* -pfd < final, ga, release
349+
* 2.0.1.MR < 2.0.1
350+
* 9.4.1.jre16 > 9.4.1.jre16-preview
351+
*/
352+
@Test
353+
public void testMng7559() {
354+
// checking general cases
355+
checkVersionsOrder(new String[] {"ab", "alpha", "beta", "cd", "ea", "milestone", "mr", "pfd", "preview", "RC"});
356+
// checking identified issues respect the general case
357+
checkVersionsOrder("2.3-pfd", "2.3");
358+
checkVersionsOrder("2.0.1.MR", "2.0.1");
359+
checkVersionsOrder("9.4.1.jre16-preview", "9.4.1.jre16");
360+
checkVersionsOrder("1-sp-1", "1-ga-1"); // proving website documentation right.
361+
}
362+
350363
/**
351364
* Test <a href="https://issues.apache.org/jira/browse/MNG-7644">MNG-7644</a> edge cases
352365
* 1.0.0.RC1 < 1.0.0-RC2 and more generally:

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
@@ -119,7 +119,7 @@ public void testVersionComparing() {
119119
assertVersionOlder("2.0-1", "2.0.1");
120120

121121
assertVersionOlder("2.0.1-klm", "2.0.1-lmn");
122-
assertVersionOlder("2.0.1", "2.0.1-xyz");
122+
assertVersionOlder("2.0.1-xyz", "2.0.1"); // now 2.0.1-xyz < 2.0.1 as of MNG-7559
123123
assertVersionOlder("2.0.1-xyz-1", "2.0.1-1-xyz");
124124

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

0 commit comments

Comments
 (0)