diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/ContainerGroupRepositoryImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/ContainerGroupRepositoryImpl.java new file mode 100644 index 000000000..8ebef892b --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/ContainerGroupRepositoryImpl.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2022 - 2023, 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 io.github.ascopes.jct.containers.ContainerGroup; +import io.github.ascopes.jct.containers.ModuleContainerGroup; +import io.github.ascopes.jct.containers.OutputContainerGroup; +import io.github.ascopes.jct.containers.PackageContainerGroup; +import io.github.ascopes.jct.filemanagers.ModuleLocation; +import io.github.ascopes.jct.utils.ModuleDiscoverer; +import io.github.ascopes.jct.workspaces.PathRoot; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.ThreadSafe; +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A repository of container groups, accessible via their {@link Location} handle. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.STABLE) +@ThreadSafe +public class ContainerGroupRepositoryImpl { + + private static final Logger LOGGER = LoggerFactory.getLogger(ContainerGroupRepositoryImpl.class); + + private final String release; + private final Map packageInputs; + private final Map moduleInputs; + private final Map outputs; + + public ContainerGroupRepositoryImpl(String release) { + this.release = release; + packageInputs = new ConcurrentHashMap<>(); + moduleInputs = new ConcurrentHashMap<>(); + outputs = new ConcurrentHashMap<>(); + } + + /** + * Add the path root to the registry, marking it as being part of the given location. + * + *

This location can be output-oriented, module-oriented, package-oriented, or a specific + * module location. Module-oriented locations (including module-oriented output locations) will + * have all modules extracted from them and registered individually rather than the provided path + * being stored. + * + * @param location the location to add. + * @param pathRoot the path root to register with the location. + */ + public void addPath(Location location, PathRoot pathRoot) { + if (location instanceof ModuleLocation) { + // If we are adding a specific module, we should resolve where it needs to live + // using custom logic so that we know it gets registered in the right place. + var moduleLocation = (ModuleLocation) location; + addModulePath(moduleLocation, pathRoot); + } else if (location.isModuleOrientedLocation()) { + // If we are adding a module-oriented location of any type, then we should discover + // the modules within it and add those. + addModuleRoot(location, pathRoot); + } else { + // If we have a regular package-oriented location, then we should just register it + // directly. + addPackageRoot(location, pathRoot); + } + } + + /** + * A bulk-style call for {@link #addPath(Location, PathRoot)}. + * + * @param location the location to add. + * @param pathRoots the path roots to register with the location. + */ + public void addPaths(Location location, Iterable pathRoots) { + pathRoots.forEach(pathRoot -> addPath(location, pathRoot)); + } + + /** + * Copy all containers from the {@code from} location to the {@code to} location. + * + * @param from the location to copy from. + * @param to the location to copy to. + * @throws IllegalArgumentException if either location is a {@link ModuleLocation}, or if the two + * locations are not the same orientation (i.e. output-oriented, + * module-oriented). + */ + public void copyContainers(Location from, Location to) { + if (from instanceof ModuleLocation || to instanceof ModuleLocation) { + throw new IllegalArgumentException( + "Cannot currently transfer individual modules to other locations" + ); + } + + if (from.isOutputLocation() && !to.isOutputLocation()) { + throw new IllegalArgumentException( + "Expected " + from.getName() + " and " + to.getName() + " to both be " + + "output locations" + ); + } + + if (from.isModuleOrientedLocation() && !to.isModuleOrientedLocation()) { + throw new IllegalArgumentException( + "Expected " + from.getName() + " and " + to.getName() + " to both be " + + "module-oriented locations" + ); + } + + if (from.isOutputLocation()) { + copyPackageContainers(from, to); + copyModuleContainers(from, to); + } else if (from.isModuleOrientedLocation()) { + copyModuleContainers(from, to); + } else { + copyPackageContainers(from, to); + } + } + + /** + * Create an empty location if it does not already exist. + * + * @param location the location to create. + * @throws IllegalArgumentException if the input is a module location. + */ + public void createEmptyLocation(Location location) { + if (location instanceof ModuleLocation) { + throw new IllegalArgumentException("Cannot ensure a module location exists"); + } + + if (location.isOutputLocation()) { + getOrCreateOutputContainerGroup(location); + } else if (location.isModuleOrientedLocation()) { + getOrCreateModuleContainerGroup(location); + } else { + getOrCreatePackageContainerGroup(location); + } + } + + /** + * Get a container group. + * + * @param location the location to get the container group for. + * @return the container group, or {@code null} if no group is associated with the location. + */ + @Nullable + public ContainerGroup getContainerGroup(Location location) { + ContainerGroup group = outputs.get(location); + if (group == null) { + group = moduleInputs.get(location); + if (group == null) { + group = packageInputs.get(location); + } + } + + return group; + } + + /** + * Get a package container group. + * + * @param location the location associated with the group to get. + * @return the container group, or {@code null} if no group is associated with the location. + */ + @Nullable + public PackageContainerGroup getPackageContainerGroup(Location location) { + return packageInputs.get(location); + } + + /** + * Get a snapshot of all the package container groups for inputs. + * + * @return the package container groups. + */ + public Collection getPackageContainerGroups() { + return Set.copyOf(packageInputs.values()); + } + + /** + * Get a package-oriented container group from the input packages or the outputs. + * + *

This also accepts {@link ModuleLocation module locations} and will resolve them + * correctly. + * + * @param location the location associated with the group to get. + * @return the container group, or {@code null} if no group is associated with the location. + */ + @Nullable + public PackageContainerGroup getPackageOrientedContainerGroup(Location location) { + if (location instanceof ModuleLocation) { + var moduleLocation = (ModuleLocation) location; + var group = getModuleOrientedContainerGroup(moduleLocation.getParent()); + return group == null + ? null + : group.getModule(moduleLocation.getModuleName()); + } + + var group = packageInputs.get(location); + return group == null + ? outputs.get(location) + : group; + } + + /** + * Get a module container group. + * + * @param location the location associated with the group to get. + * @return the container group, or {@code null} if no group is associated with the location. + */ + @Nullable + public ModuleContainerGroup getModuleContainerGroup(Location location) { + return moduleInputs.get(location); + } + + /** + * Get a snapshot of all the module container groups for inputs. + * + * @return the module container groups. + */ + public Collection getModuleContainerGroups() { + return Set.copyOf(moduleInputs.values()); + } + + /** + * Get a module-oriented container group from the input modules or the outputs. + * + * @param location the location associated with the group to get. + * @return the container group, or {@code null} if no group is associated with the location. + */ + @Nullable + public ModuleContainerGroup getModuleOrientedContainerGroup(Location location) { + var group = moduleInputs.get(location); + return group == null ? outputs.get(location) : group; + } + + /** + * Get an output container group. + * + * @param location the location associated with the group to get. + * @return the container group, or {@code null} if no group is associated with the location. + */ + @Nullable + public OutputContainerGroup getOutputContainerGroup(Location location) { + return outputs.get(location); + } + + /** + * Get a snapshot of all the output container groups. + * + * @return the output container groups. + */ + public Collection getOutputContainerGroups() { + return Set.copyOf(outputs.values()); + } + + /** + * Determine if the given location is available in this repository. + * + * @param location the location to look up. + * @return {@code true} if the location exists, or {@code false} if it does not. + */ + public boolean hasLocation(Location location) { + if (location instanceof ModuleLocation) { + var moduleLocation = (ModuleLocation) location; + var parentLocation = moduleLocation.getParent(); + var group = parentLocation.isOutputLocation() + ? outputs.get(parentLocation) + : moduleInputs.get(parentLocation); + + if (group == null) { + return false; + } + + return group.hasLocation(moduleLocation); + } + + if (location.isOutputLocation()) { + return outputs.containsKey(location); + } + + if (location.isModuleOrientedLocation()) { + return moduleInputs.containsKey(location); + } + + return packageInputs.containsKey(location); + } + + /** + * Find all {@link ModuleLocation module location objects} within a given module-oriented or + * output location. + * + * @param location the location to discover modules within. + * @return the set of module locations that were found. + */ + public Set listLocationsForModules(Location location) { + var group = location.isOutputLocation() + ? outputs.get(location) + : moduleInputs.get(location); + + return group == null + ? Set.of() + : group.getLocationsForModules(); + } + + private void addModulePath(ModuleLocation moduleLocation, PathRoot pathRoot) { + var parentLocation = moduleLocation.getParent(); + + var group = parentLocation.isOutputLocation() + ? getOrCreateOutputContainerGroup(parentLocation) + : getOrCreateModuleContainerGroup(parentLocation); + + group.addModule(moduleLocation.getModuleName(), pathRoot); + } + + private void addModuleRoot(Location location, PathRoot pathRoot) { + // Attempt to find the modules in the location. + var modules = ModuleDiscoverer.findModulesIn(pathRoot.getPath()); + + if (modules.isEmpty()) { + LOGGER.warn( + "There were no valid modules present in {}, so nothing has been registered. " + + "If you are sure that there are files, this is probably a bug.", + modules + ); + } else { + var group = location.isOutputLocation() + ? getOrCreateOutputContainerGroup(location) + : getOrCreateModuleContainerGroup(location); + + modules.forEach((moduleName, modulePath) -> { + group.addModule(moduleName, new WrappingDirectoryImpl(modulePath)); + }); + } + } + + private void addPackageRoot(Location location, PathRoot pathRoot) { + // Simplest case. We just have a package. + var group = location.isOutputLocation() + ? getOrCreateOutputContainerGroup(location) + : getOrCreatePackageContainerGroup(location); + + group.addPackage(pathRoot); + } + + private void copyPackageContainers(Location from, Location to) { + var source = from.isOutputLocation() + ? outputs.get(from) + : packageInputs.get(from); + + if (source == null || source.isEmpty()) { + // Nothing to do. + return; + } + var target = to.isOutputLocation() + ? getOrCreateOutputContainerGroup(to) + : getOrCreatePackageContainerGroup(to); + + source.getPackages().forEach(target::addPackage); + } + + private void copyModuleContainers(Location from, Location to) { + var source = from.isOutputLocation() + ? outputs.get(from) + : moduleInputs.get(from); + + if (source == null) { + // Nothing to do. + return; + } + + var target = to.isOutputLocation() + ? getOrCreateOutputContainerGroup(to) + : getOrCreateModuleContainerGroup(to); + + source.getModules().forEach((moduleLocation, containerGroup) -> containerGroup + .getPackages() + .forEach(container -> target.addModule(moduleLocation.getModuleName(), container))); + } + + @WillNotClose + private PackageContainerGroup getOrCreatePackageContainerGroup(Location location) { + return packageInputs.computeIfAbsent( + location, + packageLocation -> new PackageContainerGroupImpl(packageLocation, release) + ); + } + + @WillNotClose + private ModuleContainerGroup getOrCreateModuleContainerGroup(Location location) { + return moduleInputs.computeIfAbsent( + location, + moduleLocation -> new ModuleContainerGroupImpl(moduleLocation, release) + ); + } + + @WillNotClose + private OutputContainerGroup getOrCreateOutputContainerGroup(Location location) { + return outputs.computeIfAbsent( + location, + outputLocation -> new OutputContainerGroupImpl(outputLocation, release) + ); + } + +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java index 429c42949..56de60394 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java @@ -50,6 +50,11 @@ public final class TeeWriter extends Writer { // and the delegated output writer at the same time. private final StringBuilder builder; + /** + * Initialise the writer. + * + * @param writer the underlying writer to "tee" to. + */ public TeeWriter(@WillCloseWhenClosed Writer writer) { lock = new Object(); closed = false; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/JctFileManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/JctFileManager.java index 0c355d72f..fab348f71 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/JctFileManager.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/JctFileManager.java @@ -75,7 +75,7 @@ public interface JctFileManager extends JavaFileManager { * * @param location the location to apply an empty container for. */ - void ensureEmptyLocationExists(Location location); + void createEmptyLocation(Location location); /** * Get the container group for the given package-oriented location. diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurer.java index 3da892d15..2fcc273a1 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurer.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurer.java @@ -75,7 +75,7 @@ public JctFileManager configure(@WillNotClose JctFileManager fileManager) { case ENABLED: LOGGER.trace("Annotation processor discovery is enabled, ensuring empty location exists"); - INHERITED_AP_PATHS.values().forEach(fileManager::ensureEmptyLocationExists); + INHERITED_AP_PATHS.values().forEach(fileManager::createEmptyLocation); return fileManager; @@ -84,7 +84,7 @@ public JctFileManager configure(@WillNotClose JctFileManager fileManager) { + "into the annotation processor path"); INHERITED_AP_PATHS.forEach(fileManager::copyContainers); - INHERITED_AP_PATHS.values().forEach(fileManager::ensureEmptyLocationExists); + INHERITED_AP_PATHS.values().forEach(fileManager::createEmptyLocation); return fileManager; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerImpl.java index 5931e9f9e..c43939ff3 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerImpl.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerImpl.java @@ -17,32 +17,23 @@ import static java.util.Objects.requireNonNull; -import io.github.ascopes.jct.containers.ContainerGroup; import io.github.ascopes.jct.containers.ModuleContainerGroup; import io.github.ascopes.jct.containers.OutputContainerGroup; import io.github.ascopes.jct.containers.PackageContainerGroup; -import io.github.ascopes.jct.containers.impl.ModuleContainerGroupImpl; -import io.github.ascopes.jct.containers.impl.OutputContainerGroupImpl; -import io.github.ascopes.jct.containers.impl.PackageContainerGroupImpl; +import io.github.ascopes.jct.containers.impl.ContainerGroupRepositoryImpl; import io.github.ascopes.jct.filemanagers.JctFileManager; import io.github.ascopes.jct.filemanagers.ModuleLocation; import io.github.ascopes.jct.filemanagers.PathFileObject; import io.github.ascopes.jct.utils.ToStringBuilder; import io.github.ascopes.jct.workspaces.PathRoot; import java.io.IOException; -import java.lang.module.FindException; -import java.lang.module.ModuleFinder; -import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.ServiceLoader; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import javax.tools.FileObject; @@ -50,8 +41,6 @@ import javax.tools.JavaFileObject.Kind; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Simple implementation of a {@link JctFileManager}. @@ -63,68 +52,23 @@ @ThreadSafe public final class JctFileManagerImpl implements JctFileManager { - private static final Logger LOGGER = LoggerFactory.getLogger(JctFileManagerImpl.class); private static final int UNSUPPORTED_ARGUMENT = -1; - private final String release; - private final Map packages; - private final Map modules; - private final Map outputs; + private final ContainerGroupRepositoryImpl repository; public JctFileManagerImpl(String release) { - this.release = requireNonNull(release, "release"); - packages = new ConcurrentHashMap<>(); - modules = new ConcurrentHashMap<>(); - outputs = new ConcurrentHashMap<>(); + requireNonNull(release, "release"); + repository = new ContainerGroupRepositoryImpl(release); } @Override - public void addPath(Location location, PathRoot path) { - if (location instanceof ModuleLocation) { - var moduleLocation = (ModuleLocation) location; - - if (location.isOutputLocation()) { - getOrCreateOutput(moduleLocation.getParent()) - .addModule(moduleLocation.getModuleName(), path); - } else { - getOrCreateModule(moduleLocation.getParent()) - .addModule(moduleLocation.getModuleName(), path); - } - - } else if (location.isOutputLocation()) { - getOrCreateOutput(location) - .addPackage(path); - - } else if (location.isModuleOrientedLocation()) { - // Attempt to find modules. - var moduleGroup = getOrCreateModule(location); - - try { - for (var ref : ModuleFinder.of(path.getPath()).findAll()) { - var module = ref.descriptor().name(); - - // Right now, assume the module is not in a nested directory. Not sure if there are - // cases where this isn't true, but I spotted some weird errors with paths being appended - // to the end of JAR paths if I uncomment the following line. - moduleGroup.getOrCreateModule(module) - //.addPackage(new BasicPathWrapperImpl(pathWrapper, module)); - .addPackage(path); - } - } catch (FindException ex) { - LOGGER.trace("Dropping {} from module path as no modules were resolved", path, ex); - } - - } else { - getOrCreatePackage(location) - .addPackage(path); - } + public void addPath(Location location, PathRoot pathRoot) { + repository.addPath(location, pathRoot); } @Override - public void addPaths(Location location, Collection paths) { - for (var path : paths) { - addPath(location, path); - } + public void addPaths(Location location, Collection pathRoots) { + repository.addPaths(location, pathRoots); } @Override @@ -138,83 +82,18 @@ public boolean contains(Location location, FileObject fo) throws IOException { return false; } - var group = getExistingGroup(location); - + var group = repository.getContainerGroup(location); return group != null && group.contains((PathFileObject) fo); } @Override public void copyContainers(Location from, Location to) { - - if (from.isOutputLocation()) { - if (!to.isOutputLocation()) { - throw new IllegalArgumentException( - "Expected " + from.getName() + " and " + to.getName() + " to both be output locations" - ); - } - } - - if (from.isModuleOrientedLocation()) { - if (!to.isModuleOrientedLocation()) { - throw new IllegalArgumentException( - "Expected " + from.getName() + " and " + to.getName() + " to both be " - + "module-oriented locations" - ); - } - } - - if (from.isOutputLocation()) { - var toOutputs = getOrCreateOutput(to); - var fromOutput = outputs.get(from); - - if (fromOutput != null) { - fromOutput.getPackages().forEach(toOutputs::addPackage); - fromOutput.getModules().forEach((module, containers) -> containers - .getPackages() - .forEach(container -> toOutputs.addModule(module.getModuleName(), container))); - } - - } else if (from.isModuleOrientedLocation()) { - var toModules = getOrCreateModule(to); - var fromModule = modules.get(from); - - if (fromModule != null) { - fromModule.getModules().forEach((moduleLocation, containers) -> containers - .getPackages() - .forEach(container -> toModules.addModule(moduleLocation.getModuleName(), container))); - } - - } else { - var toPackages = getOrCreatePackage(to); - var fromPackage = packages.get(from); - - if (fromPackage != null) { - fromPackage.getPackages().forEach(toPackages::addPackage); - } - } + repository.copyContainers(from, to); } @Override - public void ensureEmptyLocationExists(Location location) { - - if (location instanceof ModuleLocation) { - var moduleLocation = (ModuleLocation) location; - - if (location.isOutputLocation()) { - getOrCreateOutput(moduleLocation.getParent()) - .getOrCreateModule(moduleLocation.getModuleName()); - - } else { - getOrCreateModule(moduleLocation.getParent()) - .getOrCreateModule(moduleLocation.getModuleName()); - } - } else if (location.isOutputLocation()) { - getOrCreateOutput(location); - } else if (location.isModuleOrientedLocation()) { - getOrCreateModule(location); - } else { - getOrCreatePackage(location); - } + public void createEmptyLocation(Location location) { + repository.createEmptyLocation(location); } @Override @@ -222,47 +101,13 @@ public void flush() { // Don't do anything else for now. } - @Override - @Nullable - public PackageContainerGroup getPackageContainerGroup(Location location) { - return packages.get(location); - } - - @Override - public Collection getPackageContainerGroups() { - return packages.values(); - } - - @Override - @Nullable - public ModuleContainerGroup getModuleContainerGroup(Location location) { - return modules.get(location); - } - - @Override - public Collection getModuleContainerGroups() { - return modules.values(); - } - - @Override - @Nullable - public OutputContainerGroup getOutputContainerGroup(Location location) { - return outputs.get(location); - } - - @Override - public Collection getOutputContainerGroups() { - return outputs.values(); - } - @Nullable @Override public ClassLoader getClassLoader(Location location) { // While we would normally enforce that we cannot get a classloader for a closed // file manager, we explicitly do not check for this as this is useful behaviour to have // retrospectively when performing assertions and tests on the resulting file manager state. - - var group = getExistingPackageOrientedOrOutputGroup(location); + var group = repository.getPackageOrientedContainerGroup(location); return group == null ? null @@ -271,64 +116,92 @@ public ClassLoader getClassLoader(Location location) { @Nullable @Override - public JavaFileObject getJavaFileForInput( + public FileObject getFileForInput( Location location, - String className, - Kind kind + String packageName, + String relativeName ) { - - var group = getExistingPackageOrientedOrOutputGroup(location); + var group = repository.getPackageOrientedContainerGroup(location); return group == null ? null - : group.getJavaFileForInput(className, kind); + : group.getFileForInput(packageName, relativeName); } @Nullable @Override - public JavaFileObject getJavaFileForOutput( + public FileObject getFileForOutput( Location location, - String className, - Kind kind, + String packageName, + String relativeName, FileObject sibling ) { + requireOutputLocation(location); + + // If we have a module, we may need to create a brand new location for it. + if (location instanceof ModuleLocation) { + var moduleLocation = ((ModuleLocation) location); + var group = repository.getOutputContainerGroup(moduleLocation.getParent()); - var group = getExistingPackageOrientedOrOutputGroup(location); + if (group != null) { + return group + .getOrCreateModule(moduleLocation.getModuleName()) + .getFileForOutput(packageName, relativeName); + } + } else { + var group = repository.getOutputContainerGroup(location); - return group == null - ? null - : group.getJavaFileForOutput(className, kind); + if (group != null) { + return group.getFileForOutput(packageName, relativeName); + } + } + + return null; } @Nullable @Override - public FileObject getFileForInput( + public JavaFileObject getJavaFileForInput( Location location, - String packageName, - String relativeName + String className, + Kind kind ) { - - var group = getExistingPackageOrientedOrOutputGroup(location); + var group = repository.getPackageOrientedContainerGroup(location); return group == null ? null - : group.getFileForInput(packageName, relativeName); + : group.getJavaFileForInput(className, kind); } @Nullable @Override - public FileObject getFileForOutput( + public JavaFileObject getJavaFileForOutput( Location location, - String packageName, - String relativeName, + String className, + Kind kind, FileObject sibling ) { + requireOutputLocation(location); - var group = getExistingPackageOrientedOrOutputGroup(location); + // If we have a module, we may need to create a brand new location for it. + if (location instanceof ModuleLocation) { + var moduleLocation = ((ModuleLocation) location); + var group = repository.getOutputContainerGroup(moduleLocation.getParent()); - return group == null - ? null - : group.getFileForOutput(packageName, relativeName); + if (group != null) { + return group + .getOrCreateModule(moduleLocation.getModuleName()) + .getJavaFileForOutput(className, kind); + } + } else { + var group = repository.getOutputContainerGroup(location); + + if (group != null) { + return group.getJavaFileForOutput(className, kind); + } + } + + return null; } @Override @@ -362,9 +235,46 @@ public Location getLocationForModule(Location location, JavaFileObject fo) { ); } + + @Override + @Nullable + public ModuleContainerGroup getModuleContainerGroup(Location location) { + requireModuleOrientedLocation(location); + return repository.getModuleContainerGroup(location); + } + + @Override + public Collection getModuleContainerGroups() { + return repository.getModuleContainerGroups(); + } + + @Override + @Nullable + public OutputContainerGroup getOutputContainerGroup(Location location) { + requireOutputLocation(location); + return repository.getOutputContainerGroup(location); + } + + @Override + public Collection getOutputContainerGroups() { + return repository.getOutputContainerGroups(); + } + + @Override + @Nullable + public PackageContainerGroup getPackageContainerGroup(Location location) { + requirePackageLocation(location); + return repository.getPackageContainerGroup(location); + } + + @Override + public Collection getPackageContainerGroups() { + return repository.getPackageContainerGroups(); + } + @Override public ServiceLoader getServiceLoader(Location location, Class service) { - var group = getExistingGroup(location); + var group = repository.getContainerGroup(location); if (group == null) { throw new NoSuchElementException( @@ -377,37 +287,25 @@ public ServiceLoader getServiceLoader(Location location, Class service @Override public boolean handleOption(String current, Iterator remaining) { - // We do not consume anything from the command line arguments in this implementation. return false; } @Override public boolean hasLocation(Location location) { - - if (location instanceof ModuleLocation) { - var moduleLocation = (ModuleLocation) location; - var group = getExistingModuleOrientedOrOutputGroup(moduleLocation.getParent()); - - return group != null && group.hasLocation(moduleLocation); - } - - return packages.containsKey(location) - || modules.containsKey(location) - || outputs.containsKey(location); + return repository.hasLocation(location); } @Nullable @Override public String inferBinaryName(Location location, JavaFileObject file) { + requirePackageOrientedLocation(location); if (!(file instanceof PathFileObject)) { return null; } - var pathFileObject = (PathFileObject) file; - - var group = getExistingPackageOrientedOrOutputGroup(location); + var group = repository.getPackageOrientedContainerGroup(location); return group == null ? null @@ -426,7 +324,6 @@ public String inferModuleName(Location location) { @Override public boolean isSameFile(@Nullable FileObject a, @Nullable FileObject b) { - // Some annotation processors provide null values here for some reason. if (a == null || b == null) { return false; @@ -447,8 +344,9 @@ public Set list( Set kinds, boolean recurse ) throws IOException { + requirePackageOrientedLocation(location); - var group = getExistingPackageOrientedOrOutputGroup(location); + var group = repository.getPackageOrientedContainerGroup(location); return group == null ? Set.of() @@ -458,104 +356,42 @@ public Set list( @Override public Iterable> listLocationsForModules(Location location) { requireOutputOrModuleOrientedLocation(location); - var group = getExistingModuleOrientedOrOutputGroup(location); - return group == null - ? List.of() - : List.of(group.getLocationsForModules()); + return List.of(repository.listLocationsForModules(location)); } @Override public String toString() { - return new ToStringBuilder(this) - .attribute("release", release) - .toString(); - } - - @Nullable - private ContainerGroup getExistingGroup(Location location) { - var group = getExistingPackageOrientedOrOutputGroup(location); - - return group == null - ? getExistingModuleOrientedOrOutputGroup(location) - : group; + return new ToStringBuilder(this).toString(); } - @Nullable - private ModuleContainerGroup getExistingModuleOrientedOrOutputGroup(Location location) { - if (location instanceof ModuleLocation) { + private static void requireOutputOrModuleOrientedLocation(Location location) { + if (!location.isOutputLocation() && !location.isModuleOrientedLocation()) { throw new IllegalArgumentException( - "Cannot get a module-oriented group from a ModuleLocation" + "Location " + location.getName() + " must be output or module-oriented" ); } - - var group = modules.get(location); - - if (group == null) { - group = outputs.get(location); - } - - return group; } - @Nullable - private PackageContainerGroup getExistingPackageOrientedOrOutputGroup(Location location) { - if (location instanceof ModuleLocation) { - var moduleLocation = (ModuleLocation) location; - - var module = modules.get(moduleLocation.getParent()); - - if (module == null) { - module = outputs.get(moduleLocation.getParent()); - } - - if (module == null) { - return null; - } - - return module.getOrCreateModule(moduleLocation.getModuleName()); - } - - var group = packages.get(location); - - if (group == null) { - group = outputs.get(location); + private static void requireModuleOrientedLocation(Location location) { + if (!location.isModuleOrientedLocation()) { + throw new IllegalArgumentException( + "Location " + location.getName() + " must be module-oriented" + ); } - - return group; } - private PackageContainerGroup getOrCreatePackage(Location location) { - if (location instanceof ModuleLocation) { - throw new IllegalArgumentException("Cannot get a package for a module location"); + private static void requireOutputLocation(Location location) { + if (!location.isOutputLocation()) { + throw new IllegalArgumentException( + "Location " + location.getName() + " must be an output location" + ); } - - return packages - .computeIfAbsent( - location, - unused -> new PackageContainerGroupImpl(location, release) - ); } - private ModuleContainerGroup getOrCreateModule(Location location) { - return modules - .computeIfAbsent( - location, - unused -> new ModuleContainerGroupImpl(location, release) - ); - } - - private OutputContainerGroup getOrCreateOutput(Location location) { - return outputs - .computeIfAbsent( - location, - unused -> new OutputContainerGroupImpl(location, release) - ); - } - - private void requireOutputOrModuleOrientedLocation(Location location) { - if (!location.isOutputLocation() && !location.isModuleOrientedLocation()) { + private void requirePackageLocation(Location location) { + if (location.isModuleOrientedLocation() || location.isOutputLocation()) { throw new IllegalArgumentException( - "Location " + location.getName() + " must be output or module-oriented" + "Location " + location.getName() + " must be an input package location" ); } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/ModuleDiscoverer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/ModuleDiscoverer.java new file mode 100644 index 000000000..82d861aa6 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/ModuleDiscoverer.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 - 2023, 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.utils; + +import static java.util.stream.Collectors.toMap; + +import java.lang.module.FindException; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReference; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Function; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Utility for discovering modules in a given path. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class ModuleDiscoverer extends UtilityClass { + + private static final Logger LOGGER = LoggerFactory.getLogger(ModuleDiscoverer.class); + + private ModuleDiscoverer() { + // Static-only class. + } + + /** + * Find all modules that exist in a given path. + * + *

This will only discover modules that contain a {@code module-info.class} + * or are an {@code Automatic-Module} in an accessible {@code MANIFEST.MF}. + * + * @param path the path to look within. + * @return a map of module names to the path of the module's package root. + */ + public static Map findModulesIn(Path path) { + try { + // TODO(ascopes): should I deal with sources here too? How should I do that? + return ModuleFinder + .of(path) + .findAll() + .stream() + .collect(toMap(nameExtractor(), pathExtractor())); + } catch (FindException ex) { + LOGGER.debug("Failed to find modules in {}, will ignore this error", path, ex); + return Map.of(); + } + } + + private static Function nameExtractor() { + return ref -> ref.descriptor().name(); + } + + private static Function pathExtractor() { + return ref -> Path.of(ref.location().orElseThrow()); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurerTest.java index e6c66a4fe..70fd3bffd 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurerTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurerTest.java @@ -65,7 +65,7 @@ void configureEnsuresAnEmptyLocationExistsWhenDiscoveryIsEnabled() { configurer.configure(fileManager); // Then - verify(fileManager).ensureEmptyLocationExists(StandardLocation.ANNOTATION_PROCESSOR_PATH); + verify(fileManager).createEmptyLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH); verifyNoMoreInteractions(fileManager); } @@ -101,7 +101,7 @@ void configureEnsuresAnEmptyLocationExistsWhenDiscoveryIsEnabledWithDependencies ordered.verify(fileManager) .copyContainers(StandardLocation.CLASS_PATH, StandardLocation.ANNOTATION_PROCESSOR_PATH); ordered.verify(fileManager) - .ensureEmptyLocationExists(StandardLocation.ANNOTATION_PROCESSOR_PATH); + .createEmptyLocation(StandardLocation.ANNOTATION_PROCESSOR_PATH); ordered.verifyNoMoreInteractions(); } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/impl/JctFileManagerImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/impl/JctFileManagerImplTest.java index 2faaa2845..406efafa5 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/impl/JctFileManagerImplTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/impl/JctFileManagerImplTest.java @@ -15,65 +15,59 @@ */ package io.github.ascopes.jct.tests.unit.filemanagers.impl; -import static org.assertj.core.api.Assertions.assertThat; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someLocation; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; +import io.github.ascopes.jct.containers.impl.ContainerGroupRepositoryImpl; import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; import io.github.ascopes.jct.workspaces.PathRoot; -import java.nio.file.Path; -import javax.tools.JavaFileManager.Location; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; @DisplayName("JctFileManagerImpl Tests") +@ExtendWith(MockitoExtension.class) class JctFileManagerImplTest { - @Test - @DisplayName("generates JctFileManager instance for a release") - void testGettingJctFileManagerImplInstance() { - assertThat(new JctFileManagerImpl("test")) - .isInstanceOf(JctFileManagerImpl.class); + JctFileManagerImpl fileManager; + ContainerGroupRepositoryImpl repository; + + @BeforeEach + void setUp() { + // Mock the construction so that we can access the internally created container group repository + // object. + try (var construction = mockConstruction(ContainerGroupRepositoryImpl.class)) { + fileManager = new JctFileManagerImpl("some-release"); + repository = construction.constructed().iterator().next(); + } } + @DisplayName("null releases are disallowed") + @SuppressWarnings({"resource", "ConstantConditions"}) @Test - @DisplayName("null release is disallowed") void testIfNullPointerExceptionThrownIfReleaseNull() { + // Then assertThatThrownBy(() -> new JctFileManagerImpl(null)) .isInstanceOf(NullPointerException.class) .hasMessage("release"); } + @DisplayName(".addPath(Location, PathRoot) adds the path to the repository") @Test - @DisplayName("adds package location to JctFileManager") - void testAddPathForPackageLocation() { - var packageLocation = mock(Location.class); + void addPathAddsThePathToTheRepository() { + // Given + var location = someLocation(); var pathRoot = mock(PathRoot.class); - var path = mock(Path.class); - // we mock path because it is needed by AbstractPackageContainerGroup - given(pathRoot.getPath()).willReturn(path); + // When + fileManager.addPath(location, pathRoot); - var jctFileManager = new JctFileManagerImpl("test"); - jctFileManager.addPath(packageLocation, pathRoot); - assertThat(jctFileManager.hasLocation(packageLocation)).isTrue(); + // Then + verify(repository).addPath(location, pathRoot); } - - @Test - @DisplayName("adds output location to JctFileManager") - void testAddPathForOutputLocation() { - var outputLocation = mock(Location.class); - var pathRoot = mock(PathRoot.class); - var path = mock(Path.class); - - // we mock path because it is needed by AbstractPackageContainerGroup - given(pathRoot.getPath()).willReturn(path); - given(outputLocation.isOutputLocation()).willReturn(true); - - var jctFileManager = new JctFileManagerImpl("test"); - jctFileManager.addPath(outputLocation, pathRoot); - assertThat(jctFileManager.hasLocation(outputLocation)).isTrue(); - } - }