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

io.github.ascopes.jct.containers.impl.AbstractPackageContainerGroup Maven / Gradle / Ivy

/*
 * Copyright (C) 2022 - 2024, the original author or authors.
 *
 * 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.github.ascopes.jct.containers.impl;

import static io.github.ascopes.jct.utils.IterableUtils.requireAtLeastOne;
import static java.util.Collections.synchronizedSet;
import static java.util.Objects.requireNonNull;

import io.github.ascopes.jct.containers.Container;
import io.github.ascopes.jct.containers.PackageContainerGroup;
import io.github.ascopes.jct.ex.JctIllegalInputException;
import io.github.ascopes.jct.filemanagers.ModuleLocation;
import io.github.ascopes.jct.filemanagers.PathFileObject;
import io.github.ascopes.jct.utils.Lazy;
import io.github.ascopes.jct.utils.ToStringBuilder;
import io.github.ascopes.jct.workspaces.PathRoot;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import javax.tools.JavaFileManager.Location;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import org.jspecify.annotations.Nullable;

/**
 * An abstract base implementation for a group of containers that relate to a specific location.
 *
 * 

This mechanism enables the ability to have locations with more than one path in them, * which is needed to facilitate the Java compiler's distributed class path, module handling, and * other important features. * * @author Ashley Scopes * @since 0.0.1 */ public abstract class AbstractPackageContainerGroup implements PackageContainerGroup { // https://docs.oracle.com/cd/E19830-01/819-4712/ablgz/index.html private static final Set ARCHIVE_EXTENSIONS = Set.of( ".ear", ".jar", ".rar", ".war", ".zip" ); /** * The location of the container group. */ private final Location location; private final String release; private final Set containers; private final Lazy classLoaderLazy; /** * Initialize this container group. * * @param location the location being represented. * @param release the release to use for multi-release JARs. */ protected AbstractPackageContainerGroup(Location location, String release) { this.location = requireNonNull(location, "location"); this.release = requireNonNull(release, "release"); containers = synchronizedSet(new LinkedHashSet<>()); classLoaderLazy = new Lazy<>(this::createClassLoader); } @Override public final String toString() { return new ToStringBuilder(this) .attribute("location", getLocation()) .attribute("containerCount", containers.size()) .toString(); } @Override public void addPackage(Container container) { containers.add(container); } @Override public void addPackage(PathRoot path) { var actualPath = path.getPath(); // Null filename implies the path is the root directory of a file system ( // like a MemoryFileSystem RAM file system we initialize elsewhere). var isArchive = actualPath.getFileName() != null && ARCHIVE_EXTENSIONS .stream() .anyMatch(actualPath.getFileName().toString().toLowerCase(Locale.ROOT)::endsWith); var container = isArchive ? new JarContainerImpl(getLocation(), path, release) : new PathWrappingContainerImpl(getLocation(), path); addPackage(container); } @Override public boolean contains(PathFileObject fileObject) { return containers .stream() .anyMatch(container -> container.contains(fileObject)); } @Override public void close() throws IOException { // Close everything in a best-effort fashion. classLoaderLazy.destroy(); var exceptions = new ArrayList(); for (var container : containers) { try { container.close(); } catch (IOException ex) { exceptions.add(ex); } } if (!exceptions.isEmpty()) { var newEx = new IOException("Containers failed to close in " + location.getName()); exceptions.forEach(newEx::addSuppressed); throw newEx; } } @Nullable @Override public Path getFile(String... fragments) { requireAtLeastOne(fragments, "fragments"); for (var container : containers) { var result = container.getFile(fragments); if (result != null) { return result; } } return null; } @Override public ClassLoader getClassLoader() { return classLoaderLazy.access(); } @Nullable @Override public PathFileObject getFileForInput(String packageName, String relativeName) { for (var container : containers) { var file = container.getFileForInput(packageName, relativeName); if (file != null) { return file; } } return null; } @Nullable @Override public PathFileObject getFileForOutput(String packageName, String relativeName) { for (var container : containers) { var file = container.getFileForOutput(packageName, relativeName); if (file != null) { return file; } } return null; } @Nullable @Override public PathFileObject getJavaFileForInput(String className, Kind kind) { for (var container : containers) { var file = container.getJavaFileForInput(className, kind); if (file != null) { return file; } } return null; } @Nullable @Override public PathFileObject getJavaFileForOutput(String className, Kind kind) { for (var container : containers) { var file = container.getJavaFileForOutput(className, kind); if (file != null) { return file; } } return null; } @Override public Location getLocation() { return location; } @Override public final List getPackages() { return List.copyOf(containers); } @Override public final String getRelease() { return release; } @Override public ServiceLoader getServiceLoader(Class service) { if (location instanceof ModuleLocation) { throw new JctIllegalInputException("Cannot load services from specific modules"); } return ServiceLoader.load(service, classLoaderLazy.access()); } @Nullable @Override public String inferBinaryName(PathFileObject fileObject) { for (var container : containers) { var name = container.inferBinaryName(fileObject); if (name != null) { return name; } } return null; } @Override public boolean isEmpty() { return containers.isEmpty(); } @Override public Set listFileObjects( String packageName, Set kinds, boolean recurse ) throws IOException { // XXX: could this be run in parallel? This probably won't make much difference // for in-memory paths, but may improve performance for disk-based paths. var collection = new HashSet(); for (var container : containers) { container.listFileObjects(packageName, kinds, recurse, collection); } return Collections.unmodifiableSet(collection); } /** * List all files in the group. * * @return all files in a multimap. * @throws IOException if an IO exception occurs reading the file system. * @since 0.6.0 */ @Override public Map> listAllFiles() throws IOException { var multimap = new LinkedHashMap>(); for (var container : getPackages()) { multimap.put(container, container.listAllFiles()); } return Collections.unmodifiableMap(multimap); } /** * Create a classloader and return it. * * @return the classloader. */ protected ClassLoader createClassLoader() { return new PackageContainerGroupUrlClassLoader(this); } }