Add support to detect installed Java versions using Mac native and SDKman.

Closes #196
This commit is contained in:
Mark Paluch
2022-01-11 08:58:17 +01:00
parent 1958127e45
commit e0f6caa043
3 changed files with 290 additions and 0 deletions

View File

@@ -67,6 +67,12 @@
<version>1.4.7</version>
</dependency>
<dependency>
<groupId>com.googlecode.plist</groupId>
<artifactId>dd-plist</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>

View File

@@ -0,0 +1,244 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.io;
import lombok.SneakyThrows;
import lombok.Value;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileFilter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.springframework.boot.system.SystemProperties;
import org.springframework.data.release.model.JavaVersion;
import org.springframework.data.release.model.Version;
import org.springframework.data.util.Lazy;
import org.springframework.util.StreamUtils;
import com.dd.plist.NSArray;
import com.dd.plist.NSDictionary;
import com.dd.plist.XMLPropertyListParser;
/**
* Utility to detect a Java runtime version.
*
* @author Mark Paluch
*/
public class JavaRuntimes {
private static final List<JdkDetector> DETECTORS = Arrays.asList(new SDKmanJdkDetector(), new MacNativeJdkDetector());
private static final Lazy<List<JdkInstallation>> JDKS = Lazy.of(() -> {
List<JdkInstallation> jdks = DETECTORS.stream().flatMap(it -> it.detect().stream()).sorted()
.collect(Collectors.toList());
Collections.reverse(jdks);
return Collections.unmodifiableList(jdks);
});
/**
* Lookup a {@link JdkInstallation} by detecting installed JDKs and applying the {@link Predicate filter}. Returns the
* first matching one or throws {@link NoSuchElementException}.
*
* @param filter
* @return
*/
public static JdkInstallation getJdk(Predicate<JdkInstallation> filter) {
return getJdk(filter, () -> "Cannot obtain required JDK");
}
/**
* Lookup a {@link JdkInstallation} by detecting installed JDKs and applying the {@link Predicate filter}. Returns the
* first matching one or throws {@link NoSuchElementException}.
*
* @param filter
* @param message
* @return
*/
public static JdkInstallation getJdk(Predicate<JdkInstallation> filter, Supplier<String> message) {
List<JdkInstallation> jdks = JDKS.get();
return jdks.stream().filter(filter).findFirst()
.orElseThrow(() -> new NoSuchElementException(String.format("%s%nAvailable JDK: %s", message.get(), jdks)));
}
/**
* JDK detection strategy.
*/
interface JdkDetector {
/**
* @return {@code true} if the detector strategy is available.
*/
boolean isAvailable();
/**
* @return a list of JDK installations.
*/
List<JdkInstallation> detect();
}
/**
* Selector to determine a {@link JdkInstallation}.
*/
public static class Selector {
private String notFoundMessage;
private Predicate<JdkInstallation> predicate;
private Selector() {
}
public static Selector builder() {
return new Selector();
}
public static Selector from(JavaVersion javaVersion) {
return builder().and(it -> javaVersion.getVersionDetector().test(it.getVersion()))
.message("Cannot find Java " + javaVersion.getName());
}
public Selector and(Predicate<JdkInstallation> predicate) {
this.predicate = this.predicate == null ? predicate : this.predicate.and(predicate);
return this;
}
public Selector notGraalVM() {
this.notFoundMessage += " (Not GraalVM)";
return and(it -> !it.getName().contains("GraalVM"));
}
public Selector message(String notFoundMessage) {
this.notFoundMessage = notFoundMessage;
return this;
}
public JdkInstallation getRequiredJdkInstallation() {
return JavaRuntimes.getJdk(predicate, () -> notFoundMessage);
}
}
/**
* Detector using the SDKman utility storing Java installations in {@code ~/.sdkman/candidates/java}.
*/
static class SDKmanJdkDetector implements JdkDetector {
private static final File sdkManJavaHome = new File(FileUtils.getUserDirectoryPath(), ".sdkman/candidates/java");
private static final Pattern CANDIDATE = Pattern.compile("(\\d+[\\.\\d+]+)[.-][-a-zA-Z]*");
@Override
public boolean isAvailable() {
return sdkManJavaHome.exists() && sdkManJavaHome.isDirectory();
}
@Override
public List<JdkInstallation> detect() {
File[] files = sdkManJavaHome.listFiles((FileFilter) new RegexFileFilter(CANDIDATE));
return Arrays.stream(files).map(it -> {
Matcher matcher = CANDIDATE.matcher(it.getName());
matcher.find();
return new JdkInstallation(Version.parse(matcher.group(1)), it.getName(), it);
}).collect(Collectors.toList());
}
}
/**
* Detector using the {@code /usr/libexec/java_home} utility storing Java installations in {@code /Libraries/Java} on
* the Mac.
*/
static class MacNativeJdkDetector implements JdkDetector {
private static final File javaHomeBinary = new File("/usr/libexec/java_home");
private static final Pattern VERSION = Pattern.compile("(\\d+(:?\\.\\d+)*)(:?_\\+.*)?");
@Override
public boolean isAvailable() {
return javaHomeBinary.exists() && SystemProperties.get("os.name").contains("Mac");
}
@Override
@SneakyThrows
public List<JdkInstallation> detect() {
Process process = new ProcessBuilder(javaHomeBinary.toString(), "-X").redirectOutput(ProcessBuilder.Redirect.PIPE)
.start();
process.waitFor(5, TimeUnit.SECONDS);
byte[] out = StreamUtils.copyToByteArray(process.getInputStream());
if (process.exitValue() != 0) {
throw new IllegalStateException(javaHomeBinary + " failed with: " + System.lineSeparator() + new String(out)
+ new String(StreamUtils.copyToByteArray(process.getErrorStream())));
}
NSArray array = (NSArray) XMLPropertyListParser.parse(new ByteArrayInputStream(out));
return Arrays.stream(array.getArray()).map(it -> {
NSDictionary dict = (NSDictionary) it;
String jvmHomePath = dict.get("JVMHomePath").toJavaObject(String.class);
String name = dict.get("JVMName").toJavaObject(String.class);
String version = dict.get("JVMVersion").toJavaObject(String.class).replace('_', '.');
Matcher matcher = VERSION.matcher(version);
matcher.find();
return new JdkInstallation(Version.parse(matcher.group(1)), name, new File(jvmHomePath));
}).collect(Collectors.toList());
}
}
@Value
public static class JdkInstallation implements Comparable<JdkInstallation> {
Version version;
String name;
File home;
@Override
public int compareTo(JdkInstallation o) {
return this.version.compareTo(o.version);
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.io;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.release.model.Version;
/**
* Unit tests for {@link JavaRuntimes}.
*
* @author Mark Paluch
*/
class JdkUtilityTests {
@Test
void shouldDiscoverCurrentJavaVersion() {
Version currentVersion = Version.parse(System.getProperty("java.version").replace('_', '.'));
JavaRuntimes.JdkInstallation jdk = JavaRuntimes.getJdk(it -> it.getVersion().is(currentVersion));
assertThat(jdk).isNotNull();
assertThat(System.getProperty("java.home")).contains(jdk.getHome().getPath());
}
}