From ff6c68ec829af0d26db6f69dbef106da4d1d63c1 Mon Sep 17 00:00:00 2001 From: Roman Shevchenko Date: Wed, 20 Dec 2017 17:43:58 +0100 Subject: [PATCH] [platform] new Java version parser --- .../com/intellij/util/lang/JavaVersion.java | 229 ++++++++++++++++++ .../com/intellij/util/lang/JavaVersionTest.kt | 99 ++++++++ 2 files changed, 328 insertions(+) create mode 100644 platform/util/src/com/intellij/util/lang/JavaVersion.java create mode 100644 platform/util/testSrc/com/intellij/util/lang/JavaVersionTest.kt diff --git a/platform/util/src/com/intellij/util/lang/JavaVersion.java b/platform/util/src/com/intellij/util/lang/JavaVersion.java new file mode 100644 index 000000000000..05e5bc568048 --- /dev/null +++ b/platform/util/src/com/intellij/util/lang/JavaVersion.java @@ -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 JEP 322 "Time-Based Release Versioning" (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; + } + + /** + *

Parses a Java version string.

+ * + *

Supports various sources, including (but not limited to):
+ * - {@code "java.*version"} system properties (a version number without any decoration)
+ * - output of "{@code java -version}" (usually "java version \"$VERSION\"")
+ * - a second line of the above command (something like to "Java(TM) SE Runtime Environment (build $VERSION)")
+ * - output of "{@code java --full-version}" ("java $VERSION")

+ * + *

See com.intellij.util.lang.JavaVersionTest for examples.

+ * + * @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 separators = new ArrayList(); + List numbers = new ArrayList(); + 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()))); + } +} \ No newline at end of file diff --git a/platform/util/testSrc/com/intellij/util/lang/JavaVersionTest.kt b/platform/util/testSrc/com/intellij/util/lang/JavaVersionTest.kt new file mode 100644 index 000000000000..5ac0cee7cc50 --- /dev/null +++ b/platform/util/testSrc/com/intellij/util/lang/JavaVersionTest.kt @@ -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) + } +} \ No newline at end of file