All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.linker.util.JavaRuntime Maven / Gradle / Ivy

/*
 * Copyright (c) 2019, 2020 Oracle and/or its affiliates.
 *
 * 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
 *
 *     http://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 io.helidon.linker.util;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleFinder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import io.helidon.build.util.FileUtils;
import io.helidon.linker.Jar;
import io.helidon.linker.ResourceContainer;

import static io.helidon.build.util.Constants.OS;
import static io.helidon.build.util.Constants.javaHome;
import static io.helidon.build.util.FileUtils.assertDir;
import static io.helidon.build.util.FileUtils.assertFile;
import static io.helidon.build.util.FileUtils.fileName;
import static io.helidon.build.util.FileUtils.findExecutableInPath;
import static io.helidon.build.util.FileUtils.listFiles;
import static io.helidon.build.util.OSType.Linux;
import static io.helidon.linker.Application.APP_DIR;
import static io.helidon.linker.util.Constants.JRI_DIR_SUFFIX;
import static java.util.Objects.requireNonNull;

/**
 * Java Runtime metadata.
 */
public final class JavaRuntime implements ResourceContainer {
    private static final AtomicReference CURRENT_JAVA_HOME_DIR = new AtomicReference<>();
    private static final String JMODS_DIR = "jmods";
    private static final String JMOD_SUFFIX = ".jmod";
    private static final String JAVA_BASE_JMOD = "java.base.jmod";
    private static final String JMOD_CLASSES_PREFIX = "classes/";
    private static final String JMOD_MODULE_INFO_PATH = JMOD_CLASSES_PREFIX + "module-info.class";
    private static final String FILE_SEP = File.separator;
    private static final String JAVA_EXEC = OS.javaExecutable();
    private static final String JAVA_CMD_PATH = "bin" + FILE_SEP + JAVA_EXEC;
    private static final String JAVA_MODULE_NAME_PREFIX = "java.";
    private static final String JDK_MODULE_NAME_PREFIX = "jdk.";
    private static final String HELIDON_JAR_NAME_PREFIX = "helidon-";
    private static final String INVALID_JRI = "This is not a valid JRI (" + JAVA_CMD_PATH + " not found): %s";
    private static final String INCOMPLETE_JDK = "The required *.jmod files (e.g. jmods/%s) are missing in this JDK: %s";
    private static final String HELIDON_JRI = "This is a custom Helidon JRI.";
    private static final String CUSTOM_JRI = "This appears to be a custom JRI.";
    private static final boolean OPEN_JDK = System.getProperty("java.vm.name").toLowerCase(Locale.ENGLISH).contains("openjdk");
    private static final String OPEN_JDK_RPM = "RPM based OpenJDK distributions provide *.jmod files in separate "
                                               + "\"java-*-openjdk-jmods\" packages: try 'yum list | grep jmods' to "
                                               + "find the package corresponding to your version.";
    private static final String OPEN_JDK_DEB = "Debian based OpenJDK distributions provide *.jmod files only in the "
                                               + "\"openjdk-*-jdk-headless\" packages.";
    private static final Map OPEN_JDK_LINUX_PACKAGING = Map.of("yum", OPEN_JDK_RPM,
                                                                               "apt", OPEN_JDK_DEB,
                                                                               "apt-get", OPEN_JDK_DEB,
                                                                               "dpkg", OPEN_JDK_DEB,
                                                                               "aptitude", OPEN_JDK_DEB);
    private final Path javaHome;
    private final Runtime.Version version;
    private final boolean isJdk;
    private final Path jmodsDir;
    private final Map modules;

    private static Path currentJavaHomeDir() {
        Path result = CURRENT_JAVA_HOME_DIR.get();
        if (result == null) {
            result = Paths.get(javaHome());
            CURRENT_JAVA_HOME_DIR.set(result);
        }
        return result;
    }

    /**
     * Ensures a valid JRI directory path, deleting if required.
     *
     * @param jriDirectory The JRI directory. May be {@code null}.
     * @param mainJar The main jar, used to create a name if {@code jriDirectory} not provided.
     * May not be {@code null}.
     * @param replaceExisting {@code true} if the directory can be deleted if already present.
     * @return The directory.
     * @throws IOException If an error occurs.
     */
    public static Path prepareJriDirectory(Path jriDirectory, Path mainJar, boolean replaceExisting) throws IOException {
        if (jriDirectory == null) {
            final String jarName = fileName(requireNonNull(mainJar));
            final String dirName = jarName.substring(0, jarName.lastIndexOf('.')) + JRI_DIR_SUFFIX;
            jriDirectory = FileUtils.WORKING_DIR.resolve(dirName);
        }
        if (Files.exists(jriDirectory)) {
            if (Files.isDirectory(jriDirectory)) {
                if (replaceExisting) {
                    FileUtils.deleteDirectory(jriDirectory);
                } else {
                    throw new IllegalArgumentException(jriDirectory + " is an existing directory");
                }
            } else {
                throw new IllegalArgumentException(jriDirectory + " is an existing file");
            }
        }
        return jriDirectory;
    }

    /**
     * Asserts that the given directory points to a valid Java Runtime.
     *
     * @param jriDirectory The directory.
     * @return The normalized, absolute directory path.
     * @throws IllegalArgumentException If the directory is not a valid JRI.
     */
    public static Path assertJri(Path jriDirectory) {
        final Path result = assertDir(jriDirectory);
        if (!isValidJri(jriDirectory)) {
            throw new IllegalArgumentException(String.format(INVALID_JRI, jriDirectory));
        }
        return result;
    }

    /**
     * Asserts that the given directory points to a valid Java Runtime containing {@code jmod} files.
     *
     * @param jdkDirectory The directory.
     * @return The normalized, absolute directory path.
     * @throws IllegalArgumentException If the directory is not a valid JDK.
     */
    public static Path assertJdk(Path jdkDirectory) {
        final Path result = assertDir(jdkDirectory);
        if (!isValidJdk(result)) {
            final StringBuilder sb = new StringBuilder().append(String.format(INCOMPLETE_JDK, JAVA_BASE_JMOD, jdkDirectory));
            incompleteJdkDetailMessage(jdkDirectory).ifPresent(detail -> sb.append(". ").append(detail));
            throw new IllegalArgumentException(sb.toString());
        }
        return result;
    }

    /**
     * Returns the path to the {@code java} executable in the given JRI directory.
     *
     * @param jriDirectory The directory.
     * @return The normalized, absolute directory path.
     * @throws IllegalArgumentException If the directory is not a valid JDK.
     */
    public static Path javaCommand(Path jriDirectory) {
        return assertFile(assertDir(jriDirectory).resolve(JAVA_CMD_PATH));
    }

    /**
     * Returns a new {@code JavaRuntime} for this JVM.
     *
     * @param assertJdk {@code} true if the result must be a valid JDK.
     * @return The new instance.
     * @throws IllegalArgumentException If this JVM is not a valid JDK.
     */
    public static JavaRuntime current(boolean assertJdk) {
        final Path currentJavaHome = currentJavaHomeDir();
        final Path jriDir = assertJdk ? assertJdk(currentJavaHome) : assertJri(currentJavaHome);
        return new JavaRuntime(jriDir, null, assertJdk);
    }

    /**
     * Returns a new {@code JavaRuntime} for the given directory, asserting that it is a valid JDK.
     *
     * @param jdkDirectory The directory.
     * @return The new instance.
     * @throws IllegalArgumentException If this JVM is not a valid JDK.
     */
    public static JavaRuntime jdk(Path jdkDirectory) {
        return new JavaRuntime(assertJdk(jdkDirectory), null, true);
    }

    /**
     * Returns a new {@code JavaRuntime} for the given directory, asserting that it is a valid JDK.
     *
     * @param jdkDirectory The directory.
     * @param version The runtime version of the given JDK. Computed if {@code null}.
     * @return The new instance.
     * @throws IllegalArgumentException If this JVM is not a valid JDK.
     */
    public static JavaRuntime jdk(Path jdkDirectory, Runtime.Version version) {
        return new JavaRuntime(assertJdk(jdkDirectory), version, true);
    }

    /**
     * Returns a new {@code JavaRuntime} for the given directory.
     *
     * @param jriDirectory The directory.
     * @param version The runtime version of the given JRI. If {@code null}, the version is computed if {@code jmod}
     * files are present otherwise an exception is thrown.
     * @return The new instance.
     * @throws IllegalArgumentException If this JVM is not a valid JRI or the runtime version cannot be computed.
     */
    public static JavaRuntime jri(Path jriDirectory, Runtime.Version version) {
        final Path jriDir = assertJri(jriDirectory);
        final boolean isJdk = isValidJdk(jriDir);
        return new JavaRuntime(jriDir, version, isJdk);
    }

    private JavaRuntime(Path javaHome, Runtime.Version version, boolean isJdk) {
        this.javaHome = assertDir(javaHome);
        this.jmodsDir = javaHome.resolve(JMODS_DIR);
        if (isJdk) {
            final List jmodFiles = listFiles(jmodsDir, fileName -> fileName.endsWith(JMOD_SUFFIX));
            this.version = isCurrent() ? Runtime.version() : findVersion();
            this.modules = jmodFiles.stream()
                                    .filter(file -> !Constants.EXCLUDED_MODULES.contains(moduleNameOf(file)))
                                    .collect(Collectors.toMap(JavaRuntime::moduleNameOf, Jar::open));
        } else if (version == null) {
            throw new IllegalArgumentException("Version required in a Java Runtime without 'jmods' dir: " + javaHome);
        } else {
            this.version = version;
            this.modules = Map.of();
        }
        this.isJdk = isJdk;
    }

    /**
     * Return the version.
     *
     * @return The version.
     */
    public Runtime.Version version() {
        return version;
    }

    /**
     * Returns the feature version.
     *
     * @return The feature version.
     */
    public String featureVersion() {
        return Integer.toString(version.feature());
    }

    /**
     * Returns the path from which this instance was built.
     *
     * @return The path.
     */
    public Path path() {
        return javaHome;
    }

    @Override
    public boolean containsResource(String resourcePath) {
        final String path = resourcePath.endsWith(".class") ? JMOD_CLASSES_PREFIX + resourcePath : resourcePath;
        return modules.values().stream().anyMatch(jar -> jar.containsResource(path));
    }

    /**
     * Returns whether or not this instance represents the current JVM.
     *
     * @return {@code true} if this instance is the current JVM.
     */
    public boolean isCurrent() {
        return javaHome.equals(currentJavaHomeDir());
    }

    /**
     * Returns the module names.
     *
     * @return The module names. Empty if this instance does not contain {@code .jmod} files.
     */
    public Set moduleNames() {
        return modules.keySet();
    }

    /**
     * Returns the {@code .jmod} file for the given name as a {@link Jar}.
     *
     * @param moduleName The module name.
     * @return The jar.
     * @throws IllegalArgumentException If the jar cannot be found.
     */
    public Jar jmod(String moduleName) {
        final Jar result = modules.get(moduleName);
        if (result == null) {
            throw new IllegalArgumentException("Cannot find .jmod file for module '" + moduleName + "' in " + path());
        }
        return result;
    }

    /**
     * Returns the path to the {@code jmods} directory.
     *
     * @return The path.
     */
    public Path jmodsDir() {
        return requireNonNull(jmodsDir);
    }

    /**
     * Ensure that the given directory exists, creating it if necessary.
     *
     * @param directory The directory. May be relative or absolute.
     * @return The directory.
     * @throws IllegalArgumentException If the directory is absolute but is not within this {@link #path()}.
     */
    public Path ensureDirectory(Path directory) {
        Path relativeDir = requireNonNull(directory);
        if (directory.isAbsolute()) {
            // Ensure that the directory is within our directory.
            relativeDir = path().relativize(directory);
        }
        return FileUtils.ensureDirectory(path().resolve(relativeDir));
    }

    /**
     * Returns the on disk size.
     *
     * @return The size, in bytes.
     * @throws UncheckedIOException If an error occurs.
     */
    public long diskSize() {
        return FileUtils.sizeOf(path());
    }

    @Override
    public String toString() {
        return (isJdk ? "JDK " : "JRI ") + version;
    }

    private Runtime.Version findVersion() {
        final Path javaBase = assertFile(jmodsDir.resolve(JAVA_BASE_JMOD));
        try (ZipFile zip = new ZipFile(javaBase.toFile())) {
            final ZipEntry entry = zip.getEntry(JMOD_MODULE_INFO_PATH);
            if (entry == null) {
                throw new IllegalStateException("Cannot find " + JMOD_MODULE_INFO_PATH + " in " + javaBase);
            }
            final ModuleDescriptor descriptor = ModuleDescriptor.read(zip.getInputStream(entry));
            return Runtime.Version.parse(descriptor.version()
                                                   .orElseThrow(() -> new IllegalStateException("No version in " + javaBase))
                                                   .toString());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static boolean isValidJri(Path jriDirectory) {
        final Path javaCommand = jriDirectory.resolve(JAVA_CMD_PATH);
        return Files.isRegularFile(javaCommand);
    }

    private static boolean isValidJdk(Path jdkDirectory) {
        if (isValidJri(jdkDirectory)) {
            final Path jmodsDir = jdkDirectory.resolve(JMODS_DIR);
            final Path javaBase = jmodsDir.resolve(JAVA_BASE_JMOD);
            return Files.isDirectory(jmodsDir) && Files.exists(javaBase);
        }
        return false;
    }

    private static Optional incompleteJdkDetailMessage(Path jdkDirectory) {
        if (isHelidonJri(jdkDirectory)) {
            return Optional.of(HELIDON_JRI);
        } else if (isCustomJri(jdkDirectory)) {
            return Optional.of(CUSTOM_JRI);
        } else if (OPEN_JDK && OS == Linux) {
            return OPEN_JDK_LINUX_PACKAGING.entrySet()
                                           .stream()
                                           .filter(e -> findExecutableInPath(e.getKey()).isPresent())
                                           .map(Map.Entry::getValue)
                                           .findFirst();
        }
        return Optional.empty();
    }

    private static boolean isCustomJri(Path jdkDirectory) {
        if (jdkDirectory.equals(currentJavaHomeDir())) {
            return ModuleFinder.ofSystem()
                               .findAll()
                               .stream()
                               .map(ref -> ref.descriptor().name())
                               .anyMatch(moduleName -> !(moduleName.startsWith(JAVA_MODULE_NAME_PREFIX)
                                                         || moduleName.startsWith(JDK_MODULE_NAME_PREFIX)));
        } else {
            return false;
        }
    }

    private static boolean isHelidonJri(Path jdkDirectory) {
        final Path appDir = jdkDirectory.resolve(APP_DIR);
        if (Files.isDirectory(appDir)) {
            return FileUtils.list(appDir, 2)
                            .stream()
                            .anyMatch(path -> path.getFileName().toString().startsWith(HELIDON_JAR_NAME_PREFIX));
        }
        return false;
    }

    private static String moduleNameOf(Path jmodFile) {
        final String fileName = fileName(jmodFile);
        return fileName.substring(0, fileName.length() - JMOD_SUFFIX.length());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy