Skip to content

Commit 37219f5

Browse files
committed
[MNG-7559] fix version comparison
1 parent c1deb62 commit 37219f5

File tree

3 files changed

+112
-44
lines changed

3 files changed

+112
-44
lines changed

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

Lines changed: 85 additions & 30 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,22 +41,39 @@
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>
55-
* <li>a hyphen usually precedes a qualifier, and is always less important than something preceded with a dot.</li>
44+
* <li>
45+
* String qualifiers are ordered lexically (case insensitive), with the following exceptions:
46+
* <ul>
47+
* <li> 'snapshot' &lt; '' &lt; 'sp' </li>
48+
* </ul>
49+
* and alias -&gt; replacement (all case insensitive):
50+
* <ul>
51+
* <li> 'a' -&gt; 'alpha' </li>
52+
* <li> 'b' -&gt; 'beta' </li>
53+
* <li> 'm' -&gt; 'milestone' </li>
54+
* <li> 'cr' -&gt; 'rc' </li>
55+
* <li> 'final' -&gt; '' </li>
56+
* <li> 'final' -&gt; '' </li>
57+
* <li> 'final' -&gt; '' </li>
58+
* </ul>
59+
* </li>
60+
* <li>
61+
* Following semver rules is encouraged, and some qualifiers are discouraged (no matter the case):
62+
* <ul>
63+
* <li> The usage of 'CR' qualifier is discouraged. Use 'RC' instead. </li>
64+
* <li> The usage of 'final', 'ga', and 'release' qualifiers is discouraged. Use no qualifier instead. </li>
65+
* <li> The usage of 'SP' qualifier is discouraged. Increment the patch version instead. </li>
66+
* </ul>
67+
* For other qualifiers, natural ordering is used (case insensitive):
68+
* <ul>
69+
* <li> alpha = a &lt; beta = b &lt; milestone = m &lt; rc = cr &lt; snapshot &lt; '' = final = ga = release &lt; sp </li>
70+
* </ul>
71+
* </li>
72+
* <li>a hyphen usually precedes a qualifier, and is always less important than digits/number, for example
73+
* 1.0.RC2 &lt; 1.0-RC3 &lt; 1.0.1 ; but prefer '1.0.0-RC1' over '1.0.0.RC1' </li>
5674
* </ul>
5775
*
58-
* @see <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">"Versioning" on Maven Wiki</a>
76+
* @see <a href="https://maven.apache.org/pom.html#Version_Order_Specification">Version Order Specification</a>
5977
* @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
6078
* @author <a href="mailto:hboutemy@apache.org">Hervé Boutemy</a>
6179
*/
@@ -119,7 +137,7 @@ public int compareTo(Item item) {
119137
switch (item.getType()) {
120138
case INT_ITEM:
121139
int itemValue = ((IntItem) item).value;
122-
return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1);
140+
return Integer.compare(value, itemValue);
123141
case LONG_ITEM:
124142
case BIGINTEGER_ITEM:
125143
return -1;
@@ -191,7 +209,7 @@ public int compareTo(Item item) {
191209
return 1;
192210
case LONG_ITEM:
193211
long itemValue = ((LongItem) item).value;
194-
return (value < itemValue) ? -1 : ((value == itemValue) ? 0 : 1);
212+
return Long.compare(value, itemValue);
195213
case BIGINTEGER_ITEM:
196214
return -1;
197215

@@ -304,23 +322,22 @@ public String toString() {
304322
* Represents a string in the version item list, usually a qualifier.
305323
*/
306324
private static class StringItem implements Item {
307-
private static final List<String> QUALIFIERS =
308-
Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp");
325+
private static final List<String> QUALIFIERS = Arrays.asList("snapshot", "", "sp");
309326

310-
private static final Properties ALIASES = new Properties();
327+
private static final Map<String, String> ALIASES = new HashMap<>(4);
311328

312329
static {
313-
ALIASES.put("ga", "");
330+
ALIASES.put("cr", "rc");
314331
ALIASES.put("final", "");
332+
ALIASES.put("ga", "");
315333
ALIASES.put("release", "");
316-
ALIASES.put("cr", "rc");
317334
}
318335

319336
/**
320-
* A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
337+
* An index value for the empty-string qualifier. This one is used to determine if a given qualifier makes
321338
* the version older than one without a qualifier, or more recent.
322339
*/
323-
private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf(""));
340+
private static final int RELEASE_VERSION_INDEX = QUALIFIERS.indexOf("");
324341

325342
private final String value;
326343

@@ -340,7 +357,7 @@ private static class StringItem implements Item {
340357
default:
341358
}
342359
}
343-
this.value = ALIASES.getProperty(value, value);
360+
this.value = ALIASES.getOrDefault(value, value);
344361
}
345362

346363
@Override
@@ -350,7 +367,7 @@ public int getType() {
350367

351368
@Override
352369
public boolean isNull() {
353-
return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0);
370+
return QUALIFIERS.indexOf(value) == RELEASE_VERSION_INDEX;
354371
}
355372

356373
/**
@@ -365,18 +382,42 @@ public boolean isNull() {
365382
*
366383
* @param qualifier
367384
* @return an equivalent value that can be used with lexical comparison
385+
* @deprecated Use {@link #compareQualifiers(String, String)} instead
368386
*/
387+
@Deprecated
369388
public static String comparableQualifier(String qualifier) {
370-
int i = QUALIFIERS.indexOf(qualifier);
389+
int index = QUALIFIERS.indexOf(qualifier) + 1;
371390

372-
return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i);
391+
return index == 0 ? ("0-" + qualifier) : String.valueOf(index);
392+
}
393+
394+
/**
395+
* Compare the qualifiers of two artifact versions.
396+
*
397+
* @param qualifier1 qualifier of first artifact
398+
* @param qualifier2 qualifier of second artifact
399+
* @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or
400+
* greater than the second
401+
*/
402+
public static int compareQualifiers(String qualifier1, String qualifier2) {
403+
int i1 = QUALIFIERS.indexOf(qualifier1);
404+
int i2 = QUALIFIERS.indexOf(qualifier2);
405+
406+
// if both pre-release, then use natural lexical ordering
407+
if (i1 == -1 && i2 == -1) {
408+
// alpha < beta < ea < milestone < preview < rc
409+
return qualifier1.compareTo(qualifier2);
410+
}
411+
412+
// 'other qualifier' < 'snapshot' < '' < 'sp'
413+
return Integer.compare(i1, i2);
373414
}
374415

375416
@Override
376417
public int compareTo(Item item) {
377418
if (item == null) {
378419
// 1-rc < 1, 1-ga > 1
379-
return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
420+
return Integer.compare(QUALIFIERS.indexOf(value), RELEASE_VERSION_INDEX);
380421
}
381422
switch (item.getType()) {
382423
case INT_ITEM:
@@ -385,7 +426,7 @@ public int compareTo(Item item) {
385426
return -1; // 1.any < 1.1 ?
386427

387428
case STRING_ITEM:
388-
return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value));
429+
return compareQualifiers(value, ((StringItem) item).value);
389430

390431
case LIST_ITEM:
391432
return -1; // 1.any < 1-1
@@ -570,6 +611,13 @@ public final void parseVersion(String version) {
570611
stack.push(list);
571612
} else if (Character.isDigit(c)) {
572613
if (!isDigit && i > startIndex) {
614+
// 1.0.0.RC1 < 1.0.0-RC2
615+
// treat .RC as -RC
616+
if (!list.isEmpty()) {
617+
list.add(list = new ListItem());
618+
stack.push(list);
619+
}
620+
573621
list.add(new StringItem(version.substring(startIndex, i), true));
574622
startIndex = i;
575623

@@ -592,6 +640,13 @@ public final void parseVersion(String version) {
592640
}
593641

594642
if (version.length() > startIndex) {
643+
// 1.0.0.RC1 < 1.0.0-RC2
644+
// treat .RC as -RC
645+
if (!isDigit && !list.isEmpty()) {
646+
list.add(list = new ListItem());
647+
stack.push(list);
648+
}
649+
595650
list.add(parseItem(isDigit, version.substring(startIndex)));
596651
}
597652

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
@@ -43,13 +43,16 @@ private Comparable newComparable(String version) {
4343
}
4444

4545
private static final String[] VERSIONS_QUALIFIER = {
46+
"1-abc",
4647
"1-alpha2snapshot",
4748
"1-alpha2",
4849
"1-alpha-123",
4950
"1-beta-2",
5051
"1-beta123",
52+
"1-def",
5153
"1-m2",
5254
"1-m11",
55+
"1-pom-1",
5356
"1-rc",
5457
"1-cr2",
5558
"1-rc123",
@@ -58,18 +61,15 @@ private Comparable newComparable(String version) {
5861
"1-sp",
5962
"1-sp2",
6063
"1-sp123",
61-
"1-abc",
62-
"1-def",
63-
"1-pom-1",
6464
"1-1-snapshot",
6565
"1-1",
6666
"1-2",
6767
"1-123"
6868
};
6969

7070
private static final String[] VERSIONS_NUMBER = {
71-
"2.0", "2-1", "2.0.a", "2.0.0.a", "2.0.2", "2.0.123", "2.1.0", "2.1-a", "2.1b", "2.1-c", "2.1-1", "2.1.0.1",
72-
"2.2", "2.123", "11.a2", "11.a11", "11.b2", "11.b11", "11.m2", "11.m11", "11", "11.a", "11b", "11c", "11m"
71+
"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",
72+
"2.123", "11.a", "11.a2", "11.a11", "11b", "11.b2", "11.b11", "11c", "11m", "11.m2", "11.m11", "11"
7373
};
7474

7575
private void checkVersionsOrder(String[] versions) {
@@ -195,7 +195,7 @@ public void testVersionComparing() {
195195

196196
checkVersionsOrder("2.0-1", "2.0.1");
197197
checkVersionsOrder("2.0.1-klm", "2.0.1-lmn");
198-
checkVersionsOrder("2.0.1", "2.0.1-xyz");
198+
checkVersionsOrder("2.0.1-xyz", "2.0.1"); // now 2.0.1-xyz < 2.0.1 as of MNG-7559
199199

200200
checkVersionsOrder("2.0.1", "2.0.1-123");
201201
checkVersionsOrder("2.0.1-xyz", "2.0.1-123");
@@ -209,13 +209,9 @@ public void testVersionComparing() {
209209
* <a href="https://netbeans.org/bugzilla/show_bug.cgi?id=226100">226100</a>
210210
*/
211211
public void testMng5568() {
212-
String a = "6.1.0";
213-
String b = "6.1.0rc3";
214-
String c = "6.1H.5-beta"; // this is the unusual version string, with 'H' in the middle
215-
216-
checkVersionsOrder(b, a); // classical
217-
checkVersionsOrder(b, c); // now b < c, but before MNG-5568, we had b > c
218-
checkVersionsOrder(a, c);
212+
checkVersionsOrder("6.1H.5-beta", "6.1.0rc3"); // now H < RC as of MNG-7559
213+
checkVersionsOrder("6.1.0rc3", "6.1.0"); // classical
214+
checkVersionsOrder("6.1H.5-beta", "6.1.0"); // transitivity
219215
}
220216

221217
/**
@@ -332,4 +328,21 @@ public void testReuse() {
332328

333329
assertEquals("reused instance should be equivalent to new instance", c1, c2);
334330
}
331+
332+
/**
333+
* Test <a href="https://issues.apache.org/jira/browse/MNG-7559">MNG-7559</a> edge cases
334+
* 1.0.0.RC1 < 1.0.0-RC2
335+
* -pfd < final, ga, release
336+
* 2.0.1.MR < 2.0.1
337+
* 9.4.1.jre16 > 9.4.1.jre16-preview
338+
*/
339+
public void testMng7559() {
340+
checkVersionsOrder("1.0.0.RC1", "1.0.0-RC2");
341+
checkVersionsOrder("4.0.0.Beta3", "4.0.0-RC2");
342+
checkVersionsOrder("2.3-pfd", "2.3");
343+
checkVersionsOrder("2.0.1.MR", "2.0.1");
344+
checkVersionsOrder("9.4.1.jre16-preview", "9.4.1.jre16");
345+
checkVersionsEqual("2.0.a", "2.0.0.a"); // previously ordered, now equals
346+
checkVersionsOrder("1-sp-1", "1-ga-1"); // proving website documentation right.
347+
}
335348
}

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
@@ -113,7 +113,7 @@ public void testVersionComparing() {
113113
assertVersionOlder("2.0-1", "2.0.1");
114114

115115
assertVersionOlder("2.0.1-klm", "2.0.1-lmn");
116-
assertVersionOlder("2.0.1", "2.0.1-xyz");
116+
assertVersionOlder("2.0.1-xyz", "2.0.1"); // now 2.0.1-xyz < 2.0.1 as of MNG-7559
117117
assertVersionOlder("2.0.1-xyz-1", "2.0.1-1-xyz");
118118

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

0 commit comments

Comments
 (0)