ComponentFactory.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 iu.type;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ModuleLayer.Controller;
import java.nio.file.Files;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.function.Consumer;

import edu.iu.IuException;
import edu.iu.IuIterable;
import edu.iu.UnsafeRunnable;
import edu.iu.type.base.CloseableModuleFinder;
import edu.iu.type.base.ModularClassLoader;
import edu.iu.type.base.TemporaryFile;

/**
 * Creates instances of {@link Component} for
 * {@link TypeSpi#createComponent(ClassLoader, ModuleLayer, Consumer, InputStream, InputStream...)}.
 */
final class ComponentFactory {

	private ComponentFactory() {
	}

	/**
	 * Checks a archive version against another archive to verify two don't have the
	 * same version name.
	 * 
	 * <p>
	 * Deletes the archive temp file before throwing
	 * {@link IllegalArgumentException} if the version names match.
	 * 
	 * @param alreadyProvidedArchive archive already in the component path
	 * @param archive                archve to verify before adding to the component
	 *                               path.
	 */
	static void checkIfAlreadyProvided(ComponentArchive alreadyProvidedArchive, ComponentArchive archive) {
		if (alreadyProvidedArchive.version().name().equals(archive.version().name())) {
			var illegalArgumentException = new IllegalArgumentException(
					archive.version() + " was already provided by " + alreadyProvidedArchive.version());

			try {
				Files.delete(archive.path());
			} catch (Throwable deleteFailure) {
				illegalArgumentException.addSuppressed(deleteFailure);
			}

			throw illegalArgumentException;
		}
	}

	/**
	 * Creates a modular component.
	 * 
	 * @param parent             parent component
	 * @param parentLoader       {@link ClassLoader} for parent delegation
	 * @param parentLayer        {@link ModuleLayer} to extend
	 * @param archives           component path
	 * @param controllerCallback receives a reference to the {@link Controller} for
	 *                           the component's module layer
	 * @param destroy            thunk for final cleanup after closing the component
	 * @return module component
	 * @throws IOException If an I/O error occurs reading from an archive
	 */
	@SuppressWarnings("resource") // Component is responsible for ModularClassLoader close
	static Component createModular(Component parent, ClassLoader parentLoader, ModuleLayer parentLayer,
			Iterable<ComponentArchive> archives, Consumer<Controller> controllerCallback, UnsafeRunnable destroy)
			throws IOException {
		final var firstComponent = archives.iterator().next();
		if (!firstComponent.kind().isModular())
			throw new IllegalArgumentException("First component must be a module");

		final String firstModuleName;
		try (final var finder = new CloseableModuleFinder(firstComponent.path())) {
			firstModuleName = finder.findAll().iterator().next().descriptor().name();
		}

		return IuException.checked(IOException.class,
				() -> IuException.initialize(new ModularClassLoader(firstComponent.kind().isWeb(),
						IuIterable.map(archives, ComponentArchive::path), parentLayer, parentLoader, c -> {

							final var firstModule = c.layer().findModule(firstModuleName).get();
							firstModule.getPackages()
									.forEach(p -> c.addOpens(firstModule, p, ComponentFactory.class.getModule()));

							if (controllerCallback != null)
								controllerCallback.accept(c);

						}), loader -> new Component(parent, loader, loader.getModuleLayer(), archives,
								() -> IuException.suppress(loader::close, destroy))));
	}

	/**
	 * Creates a component from the source queue.
	 * 
	 * @param parent             parent component
	 * @param parentLoader       {@link ClassLoader} for parent delegation
	 * @param parentLayer        {@link ModuleLayer} to extend
	 * @param controllerCallback receives a reference to {@link Controller} for the
	 *                           component's module layer
	 * @param sources            source queue; will be drained and all entries
	 *                           closed when the component is closed, or if an
	 *                           initialization error occurs.
	 * @return fully loaded component instance
	 * @throws IOException If an I/O error occurs reaching from an archive source
	 */
	static Component createFromSourceQueue(Component parent, ClassLoader parentLoader, ModuleLayer parentLayer,
			Consumer<Controller> controllerCallback, Queue<ArchiveSource> sources) throws IOException {
		Queue<ComponentArchive> archives = new ArrayDeque<>();
		Queue<ComponentVersion> unmetDependencies = new ArrayDeque<>();

		final var destroy = TemporaryFile.init(() -> {
			while (!sources.isEmpty())
				try (var source = sources.poll()) {
					dep: for (var sourceDependency : source.dependencies()) {
						if (parent != null)
							for (var version : parent.versions())
								if (version.meets(sourceDependency))
									continue dep;
						for (var archive : archives)
							if (archive.version().meets(sourceDependency))
								continue dep;
						unmetDependencies.add(sourceDependency);
					}

					var archive = ComponentArchive.from(source);
					for (var alreadyProvidedArchive : archives)
						checkIfAlreadyProvided(alreadyProvidedArchive, archive);

					var unmetDependencyIterator = unmetDependencies.iterator();
					while (unmetDependencyIterator.hasNext()) {
						final var unmetDependency = unmetDependencyIterator.next();
						if (archive.version().meets(unmetDependency))
							unmetDependencyIterator.remove();
					}
					archives.offer(archive);

					for (var bundledDependency : archive.bundledDependencies())
						sources.offer(bundledDependency);
				}

			if (!unmetDependencies.isEmpty())
				throw new IllegalArgumentException("Not all dependencies were met, missing " + unmetDependencies);

		});

		try {
			return createModular(parent, parentLoader, parentLayer, archives, controllerCallback, destroy);
		} catch (Throwable e) {
			IuException.suppress(e, destroy);
			throw e;
		}
	}

	/**
	 * Creates a component from the source inputs
	 * 
	 * @param parent                           parent component
	 * @param parentLoader                     {@link ClassLoader} for parent
	 *                                         delegation
	 * @param parentLayer                      {@link ModuleLayer} to extend
	 * @param controllerCallback               receives a reference to the
	 *                                         {@link Controller} for the
	 *                                         component's {@link ModuleLayer}
	 * @param componentArchiveSource           component archive source input
	 * @param providedDependencyArchiveSources dependency source inputs
	 * @return fully loaded component instance
	 */
	static Component createComponent(Component parent, ClassLoader parentLoader, ModuleLayer parentLayer,
			Consumer<Controller> controllerCallback, InputStream componentArchiveSource,
			InputStream... providedDependencyArchiveSources) {

		Queue<ArchiveSource> sources = new ArrayDeque<>();
		Throwable thrown = null;
		try {
			sources.offer(new ArchiveSource(componentArchiveSource));
			for (var providedDependencyArchiveSource : providedDependencyArchiveSources)
				sources.offer(new ArchiveSource(providedDependencyArchiveSource));

			return createFromSourceQueue(parent, parentLoader, parentLayer, controllerCallback, sources);

		} catch (Throwable e) {
			thrown = e;
			throw IuException.unchecked(e);
		} finally {
			final var throwing = thrown != null;
			while (!sources.isEmpty())
				thrown = IuException.suppress(thrown, sources.poll()::close);

			if (!throwing && thrown != null)
				throw IuException.unchecked(thrown);
		}
	}

}