[platform] new Java version parser

This commit is contained in:
Roman Shevchenko
2017-12-20 17:44:21 +01:00
parent 2b475bef7d
commit ff6c68ec82
2 changed files with 328 additions and 0 deletions
@@ -0,0 +1,229 @@
/*
* Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/
package com.intellij.util.lang;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A class representing a version of some Java platform - e.g. the runtime the class is loaded into, or some installed JRE.
*
* Based on <a href="http://openjdk.java.net/jeps/322">JEP 322 "Time-Based Release Versioning"</a> (Java 10+), but also supports JEP 223
* "New Version-String Scheme" (Java 9), as well as earlier version's formats.
*
* @see #parse(String) for examples of supported version strings
* @since 2018.1
*/
public final class JavaVersion {
/**
* The major version.
* Corresponds to the first number of a Java 9+ version string and to the second number of Java 1.0 to 1.8 strings.
*/
public final int feature;
/**
* The minor version.
* Corresponds to the second number of a Java 9+ version string and to the third number of Java 1.0 to 1.8 strings.
* Used in version strings prior to Java 1.5, in newer strings is always {@code 0}.
*/
public final int minor;
/**
* The patch version.
* Corresponds to the third number of a Java 9+ version string and to the number of Java 1.0 to 1.8 strings (one after an underscore).
*/
public final int update;
/**
* The build number.
* Corresponds to a number prefixed by a plus sign in a Java 9+ version string and by "-b" string in earlier versions.
*/
public final int build;
/**
* {@code true} if the platform is an early access release, {@code false} otherwise (or when not known).
*/
public final boolean ea;
private JavaVersion(int feature, int minor, int update, int build, boolean ea) {
this.feature = feature;
this.minor = minor;
this.update = update;
this.build = build;
this.ea = ea;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof JavaVersion)) return false;
JavaVersion other = (JavaVersion)o;
return feature == other.feature &&
minor == other.minor &&
update == other.update &&
build == other.build &&
ea == other.ea;
}
@Override
public int hashCode() {
int hash = feature;
hash = 31 * hash + minor;
hash = 31 * hash + update;
hash = 31 * hash + build;
hash = 31 * hash + (ea ? 1231 : 1237);
return hash;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (feature > 8) {
sb.append(feature);
if (minor > 0 || update > 0) sb.append('.').append(minor).append('.').append(update);
if (ea) sb.append("-ea");
if (build > 0) sb.append('+').append(build);
}
else {
sb.append("1.").append(feature).append('.').append(minor);
if (update > 0 || ea) sb.append('_').append(update);
if (ea) sb.append("-ea");
if (build > 0) sb.append("-b").append(build);
}
return sb.toString();
}
/**
* Composes a version object out of given parameters.
*
* @throws IllegalArgumentException when any of numbers is negative
*/
public static @NotNull JavaVersion compose(int feature, int minor, int update, int build, boolean ea) throws IllegalArgumentException {
if (feature < 0) throw new IllegalArgumentException();
if (minor < 0) throw new IllegalArgumentException();
if (update < 0) throw new IllegalArgumentException();
if (build < 0) throw new IllegalArgumentException();
return new JavaVersion(feature, minor, update, build, ea);
}
private static JavaVersion current;
/**
* Returns the version of a Java runtime the class is loaded into.
* The method attempts to parse {@code "java.runtime.version"} system property first (usually, it is more complete),
* and falls back to {@code "java.version"} if the former is invalid or differs in {@link #feature} or {@link #minor} numbers.
*/
public static @NotNull JavaVersion current() {
if (current == null) {
JavaVersion fallback = parse(System.getProperty("java.version")), rt = null;
try {
rt = parse(System.getProperty("java.runtime.version"));
}
catch (Throwable ignored) { }
current = rt != null && rt.feature == fallback.feature && rt.minor == fallback.minor ? rt : fallback;
}
return current;
}
/**
* <p>Parses a Java version string.</p>
*
* <p>Supports various sources, including (but not limited to):<br>
* - {@code "java.*version"} system properties (a version number without any decoration)<br>
* - output of "{@code java -version}" (usually "java version \"$VERSION\"")<br>
* - a second line of the above command (something like to "Java(TM) SE Runtime Environment (build $VERSION)")<br>
* - output of "{@code java --full-version}" ("java $VERSION")</p>
*
* <p>See com.intellij.util.lang.JavaVersionTest for examples.</p>
*
* @throws IllegalArgumentException if failed to recognize the number.
*/
public static @NotNull JavaVersion parse(@NotNull String versionString) throws IllegalArgumentException {
// trimming
String str = versionString;
if (str.contains("Runtime Environment")) {
int p = str.indexOf("(build ");
if (p > 0) str = str.substring(p);
}
// partitioning
List<String> separators = new ArrayList<String>();
List<String> numbers = new ArrayList<String>();
int length = str.length(), p = 0;
boolean number = false;
while (p < length) {
int start = p;
while (p < length && Character.isDigit(str.charAt(p)) == number) p++;
String part = str.substring(start, p);
(number ? numbers : separators).add(part);
number = !number;
}
// parsing
if (!numbers.isEmpty() && !separators.isEmpty()) {
try {
int feature = Integer.parseInt(numbers.get(0)), minor = 0, update = 0, build = 0;
boolean ea = false;
if (feature > 8 && feature < 50 /*sanity check*/) {
// Java 9+
p = 1;
while (p < separators.size() && ".".equals(separators.get(p))) p++;
if (p > 1 && numbers.size() > 2) {
minor = Integer.parseInt(numbers.get(1));
update = Integer.parseInt(numbers.get(2));
}
if (p < separators.size()) {
String s = separators.get(p);
if (StringUtil.startsWithChar(s, '-')) {
ea = startsWithWord(s, "-ea") || startsWithWord(s, "-internal");
if (p < numbers.size() && StringUtil.endsWithChar(s, '+')) {
build = Integer.parseInt(numbers.get(p));
}
p++;
}
if (build == 0 && p < separators.size() && p < numbers.size() && "+".equals(separators.get(p))) {
build = Integer.parseInt(numbers.get(p));
}
}
return new JavaVersion(feature, minor, update, build, ea);
}
else if (feature == 1 && numbers.size() > 1 && separators.size() > 1 && ".".equals(separators.get(1))) {
// Java 1.0 .. 1.8
feature = Integer.parseInt(numbers.get(1));
if (feature <= 8) {
if (numbers.size() > 2 && separators.size() > 2 && ".".equals(separators.get(2))) {
minor = Integer.parseInt(numbers.get(2));
if (numbers.size() > 3 && separators.size() > 3 && "_".equals(separators.get(3))) {
update = Integer.parseInt(numbers.get(3));
if (separators.size() > 4) {
String s = separators.get(4);
if (StringUtil.startsWithChar(s, '-')) {
ea = startsWithWord(s, "-ea") || startsWithWord(s, "-internal");
}
p = 4;
while (p < separators.size() && !separators.get(p).endsWith("-b")) p++;
if (p < numbers.size()) {
build = Integer.parseInt(numbers.get(p));
}
}
}
}
return new JavaVersion(feature, minor, update, build, ea);
}
}
}
catch (NumberFormatException ignored) { }
}
throw new IllegalArgumentException(versionString);
}
private static boolean startsWithWord(String s, String word) {
return s.startsWith(word) && (s.length() == word.length() || !Character.isLetterOrDigit(s.charAt(word.length())));
}
}
@@ -0,0 +1,99 @@
/*
* Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/
package com.intellij.util.lang
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.Test
class JavaVersionTest {
@Test fun `1_0`() = doTest("1.0", 0)
@Test fun `1_2`() = doTest("1.2", 2)
@Test fun `1_4_0`() = doTest("1.4.0", 4)
@Test fun `1_4_2_30`() = doTest("1.4.2_30", 4, 2, 30, 0)
@Test fun `1_4_2_30-b03`() = doTest("1.4.2_30-b03", 4, 2, 30, 3)
@Test fun `1_8_0_152`() = doTest("1.8.0_152", 8, 0, 152, 0)
@Test fun `1_8_0_152-b16`() = doTest("1.8.0_152-b16", 8, 0, 152, 16)
@Test fun `1_8_0_162-ea`() = doTest("1.8.0_162-ea", 8, 0, 162, 0, true)
@Test fun `1_8_0_162-ea-b03`() = doTest("1.8.0_162-ea-b03", 8, 0, 162, 3, true)
@Test fun `1_8_0_152-jb`() = doTest("1.8.0_152-release", 8, 0, 152, 0)
@Test fun `1_8_0_152-jb-b13`() = doTest("1.8.0_152-release-1056-b13", 8, 0, 152, 13)
@Test fun `1_8_0_151-debian-b12`() = doTest("1.8.0_151-8u151-b12-0ubuntu0.17.10.2-b12", 8, 0, 151, 12)
@Test fun `1_8_0_45-internal`() = doTest("1.8.0_45-internal", 8, 0, 45, 0, true)
@Test fun `1_8_0_121-2-whatever-b11`() = doTest("1.8.0_121-2-whatever-b11", 8, 0, 121, 11)
@Test fun `1_8_0_121-(big-number)-b11`() = doTest("1.8.0_121-99${Long.MAX_VALUE}-b11", 8, 0, 121, 11)
@Test fun `9`() = doTest("9", 9)
@Test fun `9-ea`() = doTest("9-ea", 9, 0, 0, 0, true)
@Test fun `9-internal`() = doTest("9-internal", 9, 0, 0, 0, true)
@Test fun `9-ea+165`() = doTest("9-ea+165", 9, 0, 0, 165, true)
@Test fun `9+181`() = doTest("9+181", 9, 0, 0, 181)
@Test fun `9_0_1`() = doTest("9.0.1", 9, 0, 1, 0)
@Test fun `9_1_2`() = doTest("9.1.2", 9, 1, 2, 0)
@Test fun `9_0_1+11`() = doTest("9.0.1+11", 9, 0, 1, 11)
@Test fun `9_0_3-ea`() = doTest("9.0.3-ea", 9, 0, 3, 0, true)
@Test fun `9_0_3-ea+9`() = doTest("9.0.3-ea+9", 9, 0, 3, 9, true)
@Test fun `9-Ubuntu`() = doTest("9-Ubuntu", 9)
@Test fun `9-Ubuntu+0-9b181-4`() = doTest("9-Ubuntu+0-9b181-4", 9)
@Test fun `10`() = doTest("10", 10)
@Test fun `10-ea`() = doTest("10-ea", 10, 0, 0, 0, true)
@Test fun `10-ea+36`() = doTest("10-ea+36", 10, 0, 0, 36, true)
@Test fun `10-ea+0-valhalla`() = doTest("10-mvt_ea+0-2017-12-11-1328177.valhalla", 10)
@Test fun empty() = doFailTest("")
@Test fun spaces() = doFailTest(" ")
@Test fun randomWord() = doFailTest("whatever")
@Test fun incomplete1() = doFailTest("1")
@Test fun incomplete2() = doFailTest("-1")
@Test fun incomplete3() = doFailTest("1.")
@Test fun outOfRange1() = doFailTest("0")
@Test fun outOfRange2() = doFailTest("5")
@Test fun outOfRange3() = doFailTest("99${Long.MAX_VALUE}")
@Test fun ibmRt() = doFailTest("pxa6480sr3fp10-20160720_02 (SR3 FP10)")
@Test fun current() {
val current = JavaVersion.current()
assertThat(current.feature).isGreaterThanOrEqualTo(8)
assertThat(current.minor).isEqualTo(0)
assertThat(current.build).isGreaterThan(0)
}
private fun doTest(versionString: String,
feature: Int,
minor: Int = 0,
update: Int = 0,
build: Int = 0,
ea: Boolean = false) {
val expected = JavaVersion.compose(feature, minor, update, build, ea)
val parsed = JavaVersion.parse(versionString)
assertThat(parsed).isEqualTo(expected)
val reparsed = JavaVersion.parse(parsed.toString())
assertThat(reparsed).isEqualTo(expected)
sequenceOf(
// "java -version", 1st line
"java version \"$versionString\"", // 1.0 - 9
"openjdk version \"$versionString\"",
"java version \"$versionString\" 2018-03-20", // 10+
"openjdk version \"$versionString\" 2018-03-20",
// "java -version", 2nd line
"Classic VM (build JDK-$versionString, native threads, jit)", // 1.2
"Java(TM) 2 Runtime Environment, Standard Edition (build $versionString)", // 1.3 - 1.5
"Java(TM) SE Runtime Environment (build $versionString)", // 1.6 - 9
"OpenJDK Runtime Environment (build $versionString)",
"Java(TM) SE Runtime Environment 18.3 (build $versionString)", // 10+
"OpenJDK Runtime Environment 18.3 (build $versionString)",
// "java --full-version" (9+)
"java $versionString",
"openjdk $versionString"
).forEach { assertThat(JavaVersion.parse(it)).describedAs(it).isEqualTo(expected) }
}
private fun doFailTest(versionString: String) {
assertThatExceptionOfType(IllegalArgumentException::class.java)
.isThrownBy { JavaVersion.parse(versionString) }
.withMessage(versionString)
}
}