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.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
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.listFiles;
import static java.util.Objects.requireNonNull;

/**
 * A Java Runtime directory.
 */
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 JRI_SUFFIX = "-jri";
    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 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_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("Not a valid JRI (" + JAVA_CMD_PATH + " not found): " + 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)) {
            throw new IllegalArgumentException("Not a valid JDK (" + JAVA_BASE_JMOD + " not found): " + jdkDirectory);
        }
        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 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