-
Notifications
You must be signed in to change notification settings - Fork 286
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Copy com.intellij.util.lang.JavaVersion
from upstream Kotlin repo. See KT-73967 for details. https://youtrack.jetbrains.com/issue/KT-73967 Verified by examining the artifacts locally.
- Loading branch information
Showing
1 changed file
with
334 additions
and
0 deletions.
There are no files selected for viewing
334 changes: 334 additions & 0 deletions
334
kotlin-analysis-api/src/main/kotlin/com/intellij/util/lang/JavaVersion.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,334 @@ | ||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
package com.intellij.util.lang; | ||
|
||
import com.intellij.ReviseWhenPortedToJDK; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
/** | ||
* <b>NB: copied from intellij to workaround KT-73967.</b> | ||
* | ||
* <p>Can be removed as soon as Kotlin depends on intellij of at least 243.23654.3.</p> | ||
* | ||
* | ||
* | ||
* <p>A class representing a version of some Java platform - e.g. the runtime the class is loaded into, or some installed JRE.</p> | ||
* | ||
* <p>Based on <a href="http://openjdk.org/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.</p> | ||
* | ||
* <p>See {@link #parse(String)} for examples of supported version strings.</p> | ||
* | ||
* @implNote the class is used in bootstrap - please use only JDK API | ||
*/ | ||
@SuppressWarnings("DuplicatedCode") | ||
public final class JavaVersion implements Comparable<JavaVersion> { | ||
/** | ||
* The major version. | ||
* Corresponds to the first number of the 9+ format (<b>9</b>.0.1) / the second number of the 1.x format (1.<b>8</b>.0_60). | ||
*/ | ||
public final int feature; | ||
|
||
/** | ||
* The minor version. | ||
* Corresponds to the second number of the 9+ format (9.<b>0</b>.1) / the third number of 1.x the format (1.8.<b>0</b>_60). | ||
* Was used in version strings prior to 1.5, in newer strings is always {@code 0}. | ||
*/ | ||
public final int minor; | ||
|
||
/** | ||
* The patch version. | ||
* Corresponds to the third number of the 9+ format (9.0.<b>1</b>) / the number after an underscore of the 1.x format (1.8.0_<b>60</b>). | ||
*/ | ||
public final int update; | ||
|
||
/** | ||
* The build number. | ||
* Corresponds to a number prefixed by the "plus" sign in the 9+ format (9.0.1+<b>7</b>) / | ||
* by "-b" string in the 1.x format (1.8.0_60-b<b>12</b>). | ||
*/ | ||
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 int compareTo(@NotNull JavaVersion o) { | ||
int diff = feature - o.feature; | ||
if (diff != 0) return diff; | ||
diff = minor - o.minor; | ||
if (diff != 0) return diff; | ||
diff = update - o.update; | ||
if (diff != 0) return diff; | ||
diff = build - o.build; | ||
if (diff != 0) return diff; | ||
return (ea ? 0 : 1) - (o.ea ? 0 : 1); | ||
} | ||
|
||
public boolean isAtLeast(int feature) { | ||
return this.feature >= feature; | ||
} | ||
|
||
@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; | ||
} | ||
|
||
/** | ||
* @return feature version string, e.g. <b>1.8</b> or <b>11</b> | ||
*/ | ||
public @NotNull String toFeatureString() { | ||
return formatVersionTo(true, true); | ||
} | ||
|
||
/** | ||
* @return feature, minor and update components of the version string, e.g. | ||
* <b>1.8.0_242</b> or <b>11.0.5</b> | ||
*/ | ||
public @NotNull String toFeatureMinorUpdateString() { | ||
return formatVersionTo(false, true); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return formatVersionTo(false, false); | ||
} | ||
|
||
private String formatVersionTo(boolean upToFeature, boolean upToUpdate) { | ||
StringBuilder sb = new StringBuilder(); | ||
if (feature > 8) { | ||
sb.append(feature); | ||
if (!upToFeature) { | ||
if (minor > 0 || update > 0) sb.append('.').append(minor); | ||
if (update > 0) sb.append('.').append(update); | ||
if (!upToUpdate) { | ||
if (ea) sb.append("-ea"); | ||
if (build > 0) sb.append('+').append(build); | ||
} | ||
} | ||
} | ||
else { | ||
sb.append("1.").append(feature); | ||
if (!upToFeature) { | ||
if (minor > 0 || update > 0 || ea || build > 0) sb.append('.').append(minor); | ||
if (update > 0) sb.append('_').append(update); | ||
if (!upToUpdate) { | ||
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 the 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); | ||
} | ||
|
||
public static @NotNull JavaVersion compose(int feature) { | ||
return compose(feature, 0, 0, 0, false); | ||
} | ||
|
||
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")); | ||
JavaVersion rt = rtVersion(); | ||
if (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; | ||
} | ||
|
||
/** | ||
* Attempts to use Runtime.version() method available since Java 9. | ||
*/ | ||
@ReviseWhenPortedToJDK("9") | ||
private static @Nullable JavaVersion rtVersion() { | ||
try { | ||
Object version = Runtime.class.getMethod("version").invoke(null); | ||
int major = (Integer)version.getClass().getMethod("major").invoke(version); | ||
int minor = (Integer)version.getClass().getMethod("minor").invoke(version); | ||
int security = (Integer)version.getClass().getMethod("security").invoke(version); | ||
Object buildOpt = version.getClass().getMethod("build").invoke(version); | ||
int build = (Integer)buildOpt.getClass().getMethod("orElse", Object.class).invoke(buildOpt, Integer.valueOf(0)); | ||
Object preOpt = version.getClass().getMethod("pre").invoke(version); | ||
boolean ea = (Boolean)preOpt.getClass().getMethod("isPresent").invoke(preOpt); | ||
return new JavaVersion(major, minor, security, build, ea); | ||
} | ||
catch (Throwable ignored) { | ||
return null; | ||
} | ||
} | ||
|
||
private static final int MAX_ACCEPTED_VERSION = 50; // sanity check | ||
|
||
/** | ||
* <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> | ||
* - values of Java compiler -source/-target/--release options ("$MAJOR", "1.$MAJOR")<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")<br> | ||
* - a line of "release" file ("JAVA_VERSION=\"$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.trim(); | ||
Map<String, String> trimmingMap = new HashMap<>(); // "substring to detect" to "substring from which to trim" | ||
trimmingMap.put("Runtime Environment", "(build "); | ||
trimmingMap.put("OpenJ9", "version "); | ||
trimmingMap.put("GraalVM", "Java "); | ||
for (String keyToDetect : trimmingMap.keySet()) { | ||
if (str.contains(keyToDetect)) { | ||
int p = str.indexOf(trimmingMap.get(keyToDetect)); | ||
if (p > 0) str = str.substring(p); | ||
} | ||
} | ||
|
||
// partitioning | ||
List<String> numbers = new ArrayList<>(), separators = 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 >= 5 && feature < MAX_ACCEPTED_VERSION) { | ||
// Java 9+; Java 5+ (short format) | ||
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 (s != null && !s.isEmpty() && s.charAt(0) == '-') { | ||
ea = startsWithWord(s, "-ea") || startsWithWord(s, "-internal"); | ||
if (p < numbers.size() && s.charAt(s.length() - 1) == '+') { | ||
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.4; Java 5+ (prefixed format) | ||
feature = Integer.parseInt(numbers.get(1)); | ||
if (feature <= MAX_ACCEPTED_VERSION) { | ||
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 (s != null && !s.isEmpty() && s.charAt(0) == '-') { | ||
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()))); | ||
} | ||
|
||
/** | ||
* A safe version of {@link #parse(String)} - returns {@code null} when unable to parse a version string. | ||
*/ | ||
public static @Nullable JavaVersion tryParse(String versionString) { | ||
if (versionString != null) { | ||
try { | ||
return parse(versionString); | ||
} | ||
catch (IllegalArgumentException ignored) { } | ||
} | ||
|
||
return null; | ||
} | ||
} |