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