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

com.hedera.fullstack.service.locator.api.ArtifactLoader Maven / Gradle / Ivy

/*
 * Copyright (C) 2023 Hedera Hashgraph, LLC
 *
 * 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 com.hedera.fullstack.service.locator.api;

import java.io.IOException;
import java.lang.module.Configuration;
import java.lang.module.FindException;
import java.lang.module.ModuleFinder;
import java.lang.module.ResolutionException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Dynamically loads Java JAR files from the file system as either class path entries or modules.
 * 

* Discovered JAR files containing either a {@code module-info.class} file or Automatic-Module-Name manifest entry will * be loaded as modules. Otherwise, the JAR file will be loaded as a class path entry. * *

* Modules will be loaded and all services will be bound. * *

* Loading classes and modules is a resource intensive operation which involves file system and reflective accesses. * Therefore, it is recommended that the {@link ArtifactLoader} class be used to load artifacts at application startup * and retained for the lifetime of the application. * * @see java.lang.module.Configuration#resolveAndBind(ModuleFinder, ModuleFinder, Collection) */ public final class ArtifactLoader { /** * The class logger, to be used for all log messages. */ private static final Logger LOGGER = LoggerFactory.getLogger(ArtifactLoader.class); /** * The artifact file extension. */ private static final String ARTIFACT_EXTENSION = "jar"; /** * The name of the {@code Automatic-Module-Name} manifest entry. */ private static final String AUTOMATIC_MODULE_NAME = "Automatic-Module-Name"; /** * The name of the {@code module-info.class} file. */ private static final String MODULE_DESCRIPTOR_FILE = "module-info.class"; /** * The list of paths to scan for loadable artifacts. */ private final List pathsToScan; /** * The list of paths to the modules loaded by the {@link ArtifactLoader}. */ private final List modulePath; /** * The list of paths to the class path entries loaded by the {@link ArtifactLoader}. */ private final List classPath; /** * The mutable class loader used to load classes and modules. */ private final MutableClassLoader classLoader; /** * The module layer containing the discovered modules which have been resolved and bound. */ private ModuleLayer moduleLayer; /** * Constructs a new instance of the {@link ArtifactLoader} class. * * @param paths the paths to scan for loadable artifacts. Paths may be either directories or JAR files. * @param parent the parent class loader to use when loading classes and modules. */ private ArtifactLoader(final List paths, final ClassLoader parent) { this.pathsToScan = Objects.requireNonNull(paths, "paths must not be null"); this.classLoader = new MutableClassLoader(new URL[0], Objects.requireNonNull(parent, "parent must not be null")); this.modulePath = new LinkedList<>(); this.classPath = new LinkedList<>(); } /** * Scans the file or directories specified by the {@code paths} parameter for loadable artifacts and returns a new * instance of the {@link ArtifactLoader} class. * * @param paths the paths to scan for loadable artifacts. Paths may be either directories or JAR files. * @return a new instance of the {@link ArtifactLoader} class. * @throws NullPointerException if {@code paths} is {@code null}. * @throws IllegalArgumentException if {@code paths} is empty. */ public static synchronized ArtifactLoader from(final Path... paths) { return from(false, null, paths); } /** * Scans the file or directories specified by the {@code paths} parameter for loadable artifacts and returns a new * instance of the {@link ArtifactLoader} class. * * @param recursive whether to recursively scan the specified directory paths for loadable artifacts. * @param paths the paths to scan for loadable artifacts. Paths may be either directories or JAR files. * @return a new instance of the {@link ArtifactLoader} class. * @throws NullPointerException if {@code paths} is {@code null}. * @throws IllegalArgumentException if {@code paths} is empty. */ public static synchronized ArtifactLoader from(final boolean recursive, final Path... paths) { return from(recursive, null, paths); } /** * Scans the file or directories specified by the {@code paths} parameter for loadable artifacts and returns a new * instance of the {@link ArtifactLoader} class. * * @param parent the parent {@link ArtifactLoader} to use when loading classes and modules. May be {@code null}. * @param paths the paths to scan for loadable artifacts. Paths may be either directories or JAR files. * @return a new instance of the {@link ArtifactLoader} class. * @throws NullPointerException if {@code paths} is {@code null}. * @throws IllegalArgumentException if {@code paths} is empty. */ public static synchronized ArtifactLoader from(final ArtifactLoader parent, final Path... paths) { return from(false, parent, paths); } /** * Scans the file or directories specified by the {@code paths} parameter for loadable artifacts and returns a new * instance of the {@link ArtifactLoader} class. * * @param recursive whether to recursively scan the specified directory paths for loadable artifacts. * @param parent the parent {@link ArtifactLoader} to use when loading classes and modules. May be {@code null}. * @param paths the paths to scan for loadable artifacts. Paths may be either directories or JAR files. * @return a new instance of the {@link ArtifactLoader} class. * @throws NullPointerException if {@code paths} is {@code null}. * @throws IllegalArgumentException if {@code paths} is empty. */ public static synchronized ArtifactLoader from( final boolean recursive, final ArtifactLoader parent, final Path... paths) { Objects.requireNonNull(paths, "paths must not be null"); if (paths.length == 0) { throw new IllegalArgumentException("paths must not be empty"); } final List pathsToScan = List.of(paths); final ClassLoader pcl = parent != null ? parent.classLoader() : ClassLoader.getSystemClassLoader(); final ModuleLayer parentLayer = parent != null ? parent.moduleLayer() : ModuleLayer.boot(); final ArtifactLoader loader = new ArtifactLoader(pathsToScan, pcl); loader.identifyArtifacts(recursive); loader.loadClassPath(); loader.loadModules(parentLayer != null ? parentLayer : ModuleLayer.boot()); return loader; } /** * The class loader used for the class path entries. * * @return the class loader. */ public ClassLoader classLoader() { return classLoader; } /** * The module layer containing the discovered modules which have been resolved and bound. * * @return the module layer. */ public ModuleLayer moduleLayer() { return moduleLayer; } /** * The list of individual files identified for inclusion on the class path. * * @return a list of class path files. */ public List classPath() { return Collections.unmodifiableList(classPath); } /** * The list of individual files identified for inclusion on the module path. * * @return a list of module path files. */ public List modulePath() { return Collections.unmodifiableList(modulePath); } /** * */ private void identifyArtifacts(final boolean recursive) { final PathMatcher matcher = FileSystems.getDefault().getPathMatcher(String.format("glob:*.%s", ARTIFACT_EXTENSION)); final Queue traversalQueue = new LinkedList<>(pathsToScan); while (!traversalQueue.isEmpty()) { final Path current = traversalQueue.poll(); if (Files.isRegularFile(current) && matcher.matches(current.getFileName())) { addArtifact(current); } else if (Files.isDirectory(current)) { try (final Stream stream = Files.walk(current, recursive ? Integer.MAX_VALUE : 1)) { stream.filter(Files::isRegularFile) .filter(v -> matcher.matches(v.getFileName())) .map(Path::toAbsolutePath) .forEach(this::addArtifact); } catch (final IOException e) { LOGGER.warn( String.format( "Failed to walk directory, skipping artifact identification [ path = '%s' ]", current), e); } } else { LOGGER.warn("Skipping artifact identification, file is not a JAR archive [ path = '{}' ]", current); } } } /** * Adds the specified artifact to the class path or module path. * * @param artifact the artifact to add to the class path or module path. * @throws NullPointerException if {@code artifact} is {@code null}. */ private void addArtifact(final Path artifact) { Objects.requireNonNull(artifact, "artifact must not be null"); try { if (isModule(artifact)) { modulePath.add(artifact); } else { classPath.add(artifact); } } catch (final IOException e) { LOGGER.warn( String.format( "Failed to identify artifact, an I/O error occurred [ fileName = '%s', path = '%s' ]", artifact.getFileName(), artifact), e); } } /** * Introspects a JAR artifact to determine if it is a formal module or an automatic module. * * @param artifact the artifact to check. * @return {@code true} if the specified artifact is a module, otherwise {@code false}. * @throws IOException if an I/O error occurs. * @throws NullPointerException if {@code artifact} is {@code null}. */ private boolean isModule(final Path artifact) throws IOException { Objects.requireNonNull(artifact, "artifact must not be null"); try (final JarFile jf = new JarFile(artifact.toAbsolutePath().toString())) { final JarEntry mdf = jf.getJarEntry(MODULE_DESCRIPTOR_FILE); final Manifest mf = jf.getManifest(); if (mdf == null && mf == null) { return false; } final Attributes attrs = mf.getMainAttributes(); return mdf != null || attrs.containsKey(AUTOMATIC_MODULE_NAME); } } /** * Loads the class path artifacts into the class loader. */ private void loadClassPath() { classPath.stream() .map(Path::toUri) .map(uri -> { try { return uri.toURL(); } catch (final MalformedURLException e) { LOGGER.warn( String.format( "Failed to convert path to URL, unable to load class path entry [ path = '%s' ]", uri.getPath()), e); return null; } }) .filter(Objects::nonNull) .forEach(classLoader::addURL); } /** * Loads the module path artifacts into the module layer. * * @param parentLayer the parent module layer. * @throws NullPointerException if {@code parentLayer} is {@code null}. * @throws ArtifactLoadingException if an error occurs while loading the module path artifacts. */ private void loadModules(final ModuleLayer parentLayer) { Objects.requireNonNull(parentLayer, "parentLayer must not be null"); if (modulePath.isEmpty()) { LOGGER.debug("No module path entries found, skipping module layer creation"); return; } final ModuleFinder finder = ModuleFinder.of(modulePath.toArray(new Path[0])); try { final Configuration cfg = parentLayer.configuration().resolveAndBind(finder, ModuleFinder.of(), Collections.emptySet()); moduleLayer = parentLayer.defineModulesWithOneLoader(cfg, classLoader); } catch (LayerInstantiationException | SecurityException e) { LOGGER.error("Failed to instantiate module layer, unable to load module path entries", e); throw new ArtifactLoadingException(e); } catch (FindException | ResolutionException e) { LOGGER.error("Failed to resolve modules, unable to load module path entries", e); throw new ArtifactLoadingException(e); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy