IuComponentLoader.java

/*
 * Copyright © 2025 Indiana University
 * All rights reserved.
 *
 * BSD 3-Clause License
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 * 
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * 
 * - Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.iu.type.loader;

import java.io.InputStream;
import java.lang.ModuleLayer.Controller;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.Objects;
import java.util.Queue;
import java.util.function.Consumer;

import edu.iu.IuException;
import edu.iu.IuObject;
import edu.iu.type.base.ModularClassLoader;
import edu.iu.type.base.TemporaryFile;
import iu.type.loader.LoadedComponent;

/**
 * Loads components via IU type introspection into an isolated modular
 * environment, without corrupting the application class path.
 */
public class IuComponentLoader implements AutoCloseable {

	private static final URL[] TYPE_BUNDLE_MODULE_PATH = IuException.unchecked(() -> {
		final var loader = IuComponentLoader.class.getClassLoader();
		final Queue<URL> path = new ArrayDeque<>();
		path.offer(Objects.requireNonNull(loader.getResource("iu-java-type.jar")));
		path.offer(Objects.requireNonNull(loader.getResource("iu-java-type-api.jar")));
		return path.toArray(size -> new URL[size]);
	});

	private static Iterable<Path> toModulePath(Iterable<InputStream> modules) {
		final Queue<Path> path = new ArrayDeque<>();
		for (final var url : TYPE_BUNDLE_MODULE_PATH)
			path.offer(TemporaryFile.of(url));
		for (final var module : modules)
			path.offer(TemporaryFile.of(module));
		return path;
	}

	private volatile ModularClassLoader loader;
	private volatile Module typeImplModule;

	/**
	 * Constructor.
	 * 
	 * @param typeConsumerModules Iterates modules that <em>may</em> require
	 *                            iu.util.type to extend type introspection
	 */
	public IuComponentLoader(Iterable<InputStream> typeConsumerModules) {
		this(IuObject.class.getClassLoader(),
				Objects.requireNonNullElseGet(IuObject.class.getModule().getLayer(), ModuleLayer::boot),
				typeConsumerModules, c -> {
				});
	}

	/**
	 * Constructor.
	 * 
	 * @param parent              parent {@link ClassLoader}
	 * @param parentLayer         parent {@link ModuleLayer}; <em>should</em> contain
	 *                            the iu.util and iu.util.type.loader modules
	 * @param typeConsumerModules Iterates modules that <em>may</em> require
	 *                            iu.util.type to extend type introspection
	 * @param controllerCallback  accepts a {@link Controller} for the
	 *                            {@link ModuleLayer} that includes iu.util.type and
	 *                            all modules defined by typeConsumerModules
	 */
	public IuComponentLoader(ClassLoader parent, ModuleLayer parentLayer, Iterable<InputStream> typeConsumerModules,
			Consumer<Controller> controllerCallback) {
		loader = ModularClassLoader.of(parent, parentLayer, () -> toModulePath(typeConsumerModules),
				controllerCallback);
		typeImplModule = IuException.unchecked(() -> {
			final var typeBundle = loader.loadClass("edu.iu.type.bundle.IuTypeBundle");
			final var getModule = typeBundle.getMethod("getModule");
			return (Module) getModule.invoke(null);
		});
	}

	/**
	 * Loads a component
	 * 
	 * @param controllerCallback               receives a reference to an
	 *                                         {@link Controller} that may be used
	 *                                         to set up access rules for the
	 *                                         component. This reference <em>should
	 *                                         not</em> be passed beyond the scope
	 *                                         of the callback; see
	 *                                         {@link ModularClassLoader}
	 * @param componentArchiveSource           {@link InputStream} for reading the
	 *                                         <strong>component archive</strong>.
	 * @param providedDependencyArchiveSources {@link InputStream}s for reading all
	 *                                         <strong>provided dependency
	 *                                         archives</strong>.
	 * @return {@link IuLoadedComponent}
	 */
	public IuLoadedComponent load(Consumer<Controller> controllerCallback, InputStream componentArchiveSource,
			InputStream... providedDependencyArchiveSources) {
		final var loadedComponent = new LoadedComponent(loader, loader.getModuleLayer(), controllerCallback,
				componentArchiveSource, providedDependencyArchiveSources);
		return loadedComponent;
	}

	/**
	 * Gets the component's {@link ClassLoader}.
	 * 
	 * @return {@link ClassLoader}
	 */
	public ModularClassLoader getLoader() {
		if (loader == null)
			throw new IllegalStateException("closed");
		else
			return loader;
	}

	@Override
	public synchronized void close() throws Exception {
		if (typeImplModule != null)
			typeImplModule = null;

		if (loader != null) {
			loader.close();
			loader = null;
		}
	}

}