Skip to content

Commit 7b90966

Browse files
authored
Provide common Java version detection (#921)
Provide common JVM version utilities
1 parent 738f8ad commit 7b90966

File tree

6 files changed

+481
-100
lines changed

6 files changed

+481
-100
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/*
2+
* Copyright 2016-2021 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless;
17+
18+
import java.io.File;
19+
import java.util.Arrays;
20+
import java.util.Comparator;
21+
import java.util.Map;
22+
import java.util.Map.Entry;
23+
import java.util.NavigableMap;
24+
import java.util.Objects;
25+
import java.util.TreeMap;
26+
import java.util.regex.Matcher;
27+
import java.util.regex.Pattern;
28+
import java.util.stream.Collectors;
29+
30+
import javax.annotation.Nullable;
31+
32+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
33+
34+
/** Java virtual machine helper */
35+
public final class Jvm {
36+
private static final int VERSION;
37+
38+
static {
39+
String jre = System.getProperty("java.version");
40+
if (jre.startsWith("1.8")) {
41+
VERSION = 8;
42+
} else {
43+
Matcher matcher = Pattern.compile("(\\d+)").matcher(jre);
44+
if (!matcher.find()) {
45+
throw new IllegalArgumentException("Expected " + jre + " to start with an integer");
46+
}
47+
VERSION = Integer.parseInt(matcher.group(1));
48+
if (VERSION <= 8) {
49+
throw new IllegalArgumentException("Expected " + jre + " to start with an integer greater than 8");
50+
}
51+
}
52+
}
53+
54+
/** @return the major version of this VM, e.g. 8, 9, 10, 11, 13, etc. */
55+
public static int version() {
56+
return VERSION;
57+
}
58+
59+
/**
60+
* Utility to map constraints of formatter to this JVM
61+
* @param <V> Version type of formatter
62+
*/
63+
public static class Support<V> {
64+
private final String fmtName;
65+
private final Comparator<? super V> fmtVersionComparator;
66+
private final NavigableMap<Integer, V> jvm2fmtVersion;
67+
private final NavigableMap<V, Integer> fmt2jvmVersion;
68+
69+
private Support(String fromatterName) {
70+
this(fromatterName, new SemanticVersionComparator<V>());
71+
}
72+
73+
private Support(String formatterName, Comparator<? super V> formatterVersionComparator) {
74+
fmtName = formatterName;
75+
fmtVersionComparator = formatterVersionComparator;
76+
jvm2fmtVersion = new TreeMap<Integer, V>();
77+
fmt2jvmVersion = new TreeMap<V, Integer>(formatterVersionComparator);
78+
}
79+
80+
/**
81+
* Add supported formatter version
82+
* @param minimumJvmVersion Minimum Java version required
83+
* @param maxFormatterVersion Maximum formatter version supported by the Java version
84+
* @return this
85+
*/
86+
public Support<V> add(int minimumJvmVersion, V maxFormatterVersion) {
87+
Objects.requireNonNull(maxFormatterVersion);
88+
if (null != jvm2fmtVersion.put(minimumJvmVersion, maxFormatterVersion)) {
89+
throw new IllegalArgumentException(String.format("Added duplicate entry for JVM %d+.", minimumJvmVersion));
90+
}
91+
if (null != fmt2jvmVersion.put(maxFormatterVersion, minimumJvmVersion)) {
92+
throw new IllegalArgumentException(String.format("Added duplicate entry for formatter version %s.", maxFormatterVersion));
93+
}
94+
Map.Entry<Integer, V> lower = jvm2fmtVersion.lowerEntry(minimumJvmVersion);
95+
if ((null != lower) && (fmtVersionComparator.compare(maxFormatterVersion, lower.getValue()) <= 0)) {
96+
throw new IllegalArgumentException(String.format("%d/%s should be lower than %d/%s", minimumJvmVersion, maxFormatterVersion, lower.getKey(), lower.getValue()));
97+
}
98+
Map.Entry<Integer, V> higher = jvm2fmtVersion.higherEntry(minimumJvmVersion);
99+
if ((null != higher) && (fmtVersionComparator.compare(maxFormatterVersion, higher.getValue()) >= 0)) {
100+
throw new IllegalArgumentException(String.format("%d/%s should be higher than %d/%s", minimumJvmVersion, maxFormatterVersion, higher.getKey(), higher.getValue()));
101+
}
102+
return this;
103+
}
104+
105+
/** @return Highest formatter version recommended for this JVM (null, if JVM not supported) */
106+
@Nullable
107+
public V getRecommendedFormatterVersion() {
108+
Integer configuredJvmVersionOrNull = jvm2fmtVersion.floorKey(Jvm.version());
109+
return (null == configuredJvmVersionOrNull) ? null : jvm2fmtVersion.get(configuredJvmVersionOrNull);
110+
}
111+
112+
/**
113+
* Assert the formatter is supported
114+
* @param formatterVersion Formatter version
115+
* @throws IllegalArgumentException if {@code formatterVersion} not supported
116+
*/
117+
public void assertFormatterSupported(V formatterVersion) {
118+
Objects.requireNonNull(formatterVersion);
119+
String error = buildUnsupportedFormatterMessage(formatterVersion);
120+
if (!error.isEmpty()) {
121+
throw new IllegalArgumentException(error);
122+
}
123+
}
124+
125+
private String buildUnsupportedFormatterMessage(V fmtVersion) {
126+
int requiredJvmVersion = getRequiredJvmVersion(fmtVersion);
127+
if (Jvm.version() < requiredJvmVersion) {
128+
return buildUpgradeJvmMessage(fmtVersion) + "Upgrade your JVM or try " + toString();
129+
}
130+
return "";
131+
}
132+
133+
private String buildUpgradeJvmMessage(V fmtVersion) {
134+
StringBuilder builder = new StringBuilder();
135+
builder.append(String.format("You are running Spotless on JVM %d", Jvm.version()));
136+
V recommendedFmtVersionOrNull = getRecommendedFormatterVersion();
137+
if (null != recommendedFmtVersionOrNull) {
138+
builder.append(String.format(", which limits you to %s %s.%n", fmtName, recommendedFmtVersionOrNull));
139+
} else {
140+
Entry<V, Integer> nextFmtVersionOrNull = fmt2jvmVersion.ceilingEntry(fmtVersion);
141+
if (null != nextFmtVersionOrNull) {
142+
builder.append(String.format(". %s %s requires JVM %d+", fmtName, fmtVersion, nextFmtVersionOrNull.getValue()));
143+
}
144+
builder.append(String.format(".%n"));
145+
}
146+
return builder.toString();
147+
}
148+
149+
private int getRequiredJvmVersion(V fmtVersion) {
150+
Entry<V, Integer> entry = fmt2jvmVersion.ceilingEntry(fmtVersion);
151+
if (null == entry) {
152+
entry = fmt2jvmVersion.lastEntry();
153+
}
154+
if (null != entry) {
155+
V maxKnownFmtVersion = jvm2fmtVersion.get(entry.getValue());
156+
if (fmtVersionComparator.compare(fmtVersion, maxKnownFmtVersion) <= 0) {
157+
return entry.getValue();
158+
}
159+
}
160+
return 0;
161+
}
162+
163+
/**
164+
* Suggest to use a different formatter version if formatting fails
165+
* @param formatterVersion Formatter version
166+
* @param originalFunc Formatter function
167+
* @return Wrapped formatter function. Adding hint about later versions to exceptions.
168+
*/
169+
public FormatterFunc suggestLaterVersionOnError(V formatterVersion, FormatterFunc originalFunc) {
170+
Objects.requireNonNull(formatterVersion);
171+
Objects.requireNonNull(originalFunc);
172+
final String hintUnsupportedProblem = buildUnsupportedFormatterMessage(formatterVersion);
173+
final String proposeDiffererntFormatter = hintUnsupportedProblem.isEmpty() ? buildUpgradeFormatterMessage(formatterVersion) : hintUnsupportedProblem;
174+
return proposeDiffererntFormatter.isEmpty() ? originalFunc : new FormatterFunc() {
175+
176+
@Override
177+
public String apply(String unix, File file) throws Exception {
178+
try {
179+
return originalFunc.apply(unix, file);
180+
} catch (Exception e) {
181+
throw new Exception(proposeDiffererntFormatter, e);
182+
}
183+
}
184+
185+
@Override
186+
public String apply(String input) throws Exception {
187+
try {
188+
return originalFunc.apply(input);
189+
} catch (Exception e) {
190+
throw new Exception(proposeDiffererntFormatter, e);
191+
}
192+
}
193+
194+
};
195+
}
196+
197+
private String buildUpgradeFormatterMessage(V fmtVersion) {
198+
StringBuilder builder = new StringBuilder();
199+
V recommendedFmtVersionOrNull = getRecommendedFormatterVersion();
200+
if (null != recommendedFmtVersionOrNull && (fmtVersionComparator.compare(fmtVersion, recommendedFmtVersionOrNull) < 0)) {
201+
builder.append(String.format("You are not using latest version on JVM %d+.%n", getRequiredJvmVersion(recommendedFmtVersionOrNull)));
202+
builder.append(String.format("Try to upgrade to %s %s, which may have fixed this problem.", fmtName, getRecommendedFormatterVersion()));
203+
} else {
204+
V higherFormatterVersionOrNull = fmt2jvmVersion.higherKey(fmtVersion);
205+
if (null != higherFormatterVersionOrNull) {
206+
builder.append(buildUpgradeJvmMessage(fmtVersion));
207+
Integer higherJvmVersion = fmt2jvmVersion.get(higherFormatterVersionOrNull);
208+
builder.append(String.format("If you upgrade your JVM to %d+, then you can use %s %s, which may have fixed this problem.", higherJvmVersion, fmtName, higherFormatterVersionOrNull));
209+
}
210+
}
211+
return builder.toString();
212+
}
213+
214+
@Override
215+
public String toString() {
216+
return String.format("%s alternatives:%n", fmtName) +
217+
jvm2fmtVersion.entrySet().stream().map(
218+
e -> String.format("- Version %s requires JVM %d+", e.getValue(), e.getKey())).collect(Collectors.joining(System.lineSeparator()));
219+
}
220+
221+
@SuppressFBWarnings("SE_COMPARATOR_SHOULD_BE_SERIALIZABLE")
222+
private static class SemanticVersionComparator<V> implements Comparator<V> {
223+
224+
@Override
225+
public int compare(V version0, V version1) {
226+
Objects.requireNonNull(version0);
227+
Objects.requireNonNull(version1);
228+
int[] version0Items = convert(version0);
229+
int[] version1Items = convert(version1);
230+
int numberOfElements = version0Items.length > version1Items.length ? version0Items.length : version1Items.length;
231+
version0Items = Arrays.copyOf(version0Items, numberOfElements);
232+
version1Items = Arrays.copyOf(version1Items, numberOfElements);
233+
for (int i = 0; i < numberOfElements; i++) {
234+
if (version0Items[i] > version1Items[i]) {
235+
return 1;
236+
} else if (version1Items[i] > version0Items[i]) {
237+
return -1;
238+
}
239+
}
240+
return 0;
241+
}
242+
243+
private static <V> int[] convert(V versionObject) {
244+
try {
245+
return Arrays.asList(versionObject.toString().split("\\.")).stream().mapToInt(s -> Integer.valueOf(s)).toArray();
246+
} catch (Exception e) {
247+
throw new IllegalArgumentException(String.format("Not a semantic version: %s", versionObject), e);
248+
}
249+
}
250+
};
251+
}
252+
253+
/**
254+
* Creates a map of JVM requirements for a formatter
255+
* @param <V> Version type of the formatter (V#toString() must correspond to a semantic version, separated by dots)
256+
* @param formatterName Name of the formatter
257+
* @return Empty map of supported formatters
258+
*/
259+
public static <V> Support<V> support(String formatterName) {
260+
Objects.requireNonNull(formatterName);
261+
return new Support<V>(formatterName);
262+
}
263+
}

lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java

Lines changed: 11 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@
1515
*/
1616
package com.diffplug.spotless.java;
1717

18-
import java.io.IOException;
1918
import java.io.Serializable;
2019
import java.lang.reflect.Method;
2120
import java.util.Objects;
22-
import java.util.regex.Matcher;
23-
import java.util.regex.Pattern;
2421

2522
import com.diffplug.spotless.FormatterFunc;
2623
import com.diffplug.spotless.FormatterStep;
2724
import com.diffplug.spotless.JarState;
25+
import com.diffplug.spotless.Jvm;
2826
import com.diffplug.spotless.LineEnding;
2927
import com.diffplug.spotless.Provisioner;
3028
import com.diffplug.spotless.ThrowingEx.BiFunction;
@@ -86,32 +84,13 @@ public static FormatterStep create(String version, String style, Provisioner pro
8684
State::createFormat);
8785
}
8886

89-
private static final int JRE_VERSION;
87+
static final Jvm.Support<String> JVM_SUPPORT = Jvm.<String> support(NAME).add(8, "1.7").add(11, "1.11.0");
9088

91-
static {
92-
String jre = System.getProperty("java.version");
93-
if (jre.startsWith("1.8")) {
94-
JRE_VERSION = 8;
95-
} else {
96-
Matcher matcher = Pattern.compile("(\\d+)").matcher(jre);
97-
if (!matcher.find()) {
98-
throw new IllegalArgumentException("Expected " + jre + " to start with an integer");
99-
}
100-
JRE_VERSION = Integer.parseInt(matcher.group(1));
101-
if (JRE_VERSION <= 8) {
102-
throw new IllegalArgumentException("Expected " + jre + " to start with an integer greater than 8");
103-
}
104-
}
105-
}
106-
107-
/** On JRE 11+, returns {@code 1.9}. On earlier JREs, returns {@code 1.7}. */
89+
/** Get default formatter version */
10890
public static String defaultVersion() {
109-
return JRE_VERSION >= 11 ? LATEST_VERSION_JRE_11 : LATEST_VERSION_JRE_8;
91+
return JVM_SUPPORT.getRecommendedFormatterVersion();
11092
}
11193

112-
private static final String LATEST_VERSION_JRE_8 = "1.7";
113-
private static final String LATEST_VERSION_JRE_11 = "1.11.0";
114-
11594
public static String defaultStyle() {
11695
return DEFAULT_STYLE;
11796
}
@@ -130,15 +109,16 @@ static final class State implements Serializable {
130109
final String style;
131110
final boolean reflowLongStrings;
132111

133-
State(String stepName, String version, Provisioner provisioner) throws IOException {
112+
State(String stepName, String version, Provisioner provisioner) throws Exception {
134113
this(stepName, version, DEFAULT_STYLE, provisioner);
135114
}
136115

137-
State(String stepName, String version, String style, Provisioner provisioner) throws IOException {
116+
State(String stepName, String version, String style, Provisioner provisioner) throws Exception {
138117
this(stepName, version, style, provisioner, DEFAULT_REFLOW_LONG_STRINGS);
139118
}
140119

141-
State(String stepName, String version, String style, Provisioner provisioner, boolean reflowLongStrings) throws IOException {
120+
State(String stepName, String version, String style, Provisioner provisioner, boolean reflowLongStrings) throws Exception {
121+
JVM_SUPPORT.assertFormatterSupported(version);
142122
this.jarState = JarState.from(MAVEN_COORDINATE + version, provisioner);
143123
this.stepName = stepName;
144124
this.version = version;
@@ -175,19 +155,19 @@ FormatterFunc createFormat() throws Exception {
175155

176156
BiFunction<String, Object, String> reflowLongStrings = this.reflowLongStrings ? constructReflowLongStringsFunction(classLoader, formatterClazz) : (s, f) -> s;
177157

178-
return suggestJre11(input -> {
158+
return JVM_SUPPORT.suggestLaterVersionOnError(version, (input -> {
179159
String formatted = (String) formatterMethod.invoke(formatter, input);
180160
String removedUnused = removeUnused.apply(formatted);
181161
String sortedImports = (String) importOrdererMethod.invoke(null, removedUnused);
182162
String reflowedLongStrings = reflowLongStrings.apply(sortedImports, formatter);
183163
return fixWindowsBug(reflowedLongStrings, version);
184-
});
164+
}));
185165
}
186166

187167
FormatterFunc createRemoveUnusedImportsOnly() throws Exception {
188168
ClassLoader classLoader = jarState.getClassLoader();
189169
Function<String, String> removeUnused = constructRemoveUnusedFunction(classLoader);
190-
return suggestJre11(input -> fixWindowsBug(removeUnused.apply(input), version));
170+
return JVM_SUPPORT.suggestLaterVersionOnError(version, (input -> fixWindowsBug(removeUnused.apply(input), version)));
191171
}
192172

193173
private static Function<String, String> constructRemoveUnusedFunction(ClassLoader classLoader)
@@ -262,19 +242,4 @@ static String fixWindowsBug(String input, String version) {
262242
}
263243
return input;
264244
}
265-
266-
private static FormatterFunc suggestJre11(FormatterFunc in) {
267-
if (JRE_VERSION >= 11) {
268-
return in;
269-
} else {
270-
return unixIn -> {
271-
try {
272-
return in.apply(unixIn);
273-
} catch (Exception e) {
274-
throw new Exception("You are running Spotless on JRE " + JRE_VERSION + ", which limits you to google-java-format " + LATEST_VERSION_JRE_8 + "\n"
275-
+ "If you upgrade your build JVM to 11+, then you can use google-java-format " + LATEST_VERSION_JRE_11 + ", which may have fixed this problem.", e);
276-
}
277-
};
278-
}
279-
}
280245
}

0 commit comments

Comments
 (0)