diff --git a/release-tools/pom.xml b/release-tools/pom.xml index 43fc6fa..39fb159 100644 --- a/release-tools/pom.xml +++ b/release-tools/pom.xml @@ -67,6 +67,12 @@ 1.4.7 + + com.googlecode.plist + dd-plist + 1.23 + + org.apache.commons commons-exec diff --git a/release-tools/src/main/java/org/springframework/data/release/io/JavaRuntimes.java b/release-tools/src/main/java/org/springframework/data/release/io/JavaRuntimes.java new file mode 100644 index 0000000..51b3536 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/io/JavaRuntimes.java @@ -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 DETECTORS = Arrays.asList(new SDKmanJdkDetector(), new MacNativeJdkDetector()); + private static final Lazy> JDKS = Lazy.of(() -> { + + List 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 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 filter, Supplier message) { + + List 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 detect(); + + } + + /** + * Selector to determine a {@link JdkInstallation}. + */ + public static class Selector { + + private String notFoundMessage; + private Predicate 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 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 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 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 { + + Version version; + String name; + File home; + + @Override + public int compareTo(JdkInstallation o) { + return this.version.compareTo(o.version); + } + } +} diff --git a/release-tools/src/test/java/org/springframework/data/release/io/JdkUtilityTests.java b/release-tools/src/test/java/org/springframework/data/release/io/JdkUtilityTests.java new file mode 100644 index 0000000..705a654 --- /dev/null +++ b/release-tools/src/test/java/org/springframework/data/release/io/JdkUtilityTests.java @@ -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()); + } +}