001/* 002 * Copyright (C) 2022 - 2024, the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.github.ascopes.jct.containers; 017 018import io.github.ascopes.jct.filemanagers.PathFileObject; 019import io.github.ascopes.jct.workspaces.PathRoot; 020import java.io.IOException; 021import java.nio.file.Path; 022import java.util.Collection; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import javax.tools.FileObject; 027import javax.tools.JavaFileManager.Location; 028import javax.tools.JavaFileObject; 029import javax.tools.JavaFileObject.Kind; 030import org.jspecify.annotations.Nullable; 031 032/** 033 * Base interface representing a group of package-oriented paths. 034 * 035 * <p><strong>Warning</strong>: container group APIs are not designed to allow reuse between 036 * compilation runs due the behaviour around providing access to class loaders. See the notes for 037 * {@link #getClassLoader} for more details. 038 * 039 * @author Ashley Scopes 040 * @since 0.0.1 041 */ 042public interface PackageContainerGroup extends ContainerGroup { 043 044 /** 045 * Add a container to this group. 046 * 047 * <p>The provided container will be closed when this group is closed. 048 * 049 * <p>Note that this will destroy the {@link #getClassLoader class loader} if one is already 050 * allocated from a previous request. 051 * 052 * @param container the container to add. 053 */ 054 void addPackage(Container container); 055 056 /** 057 * Add a path to this group. 058 * 059 * <p>Note that this will destroy the {@link #getClassLoader class loader} if one is already 060 * allocated from a previous request. 061 * 062 * <p>If the path points to some form of archive (such as a JAR), then this may open that archive 063 * in a new resource internally. If this occurs, then the resource will always be freed by this 064 * class by calling {@link #close}. 065 * 066 * <p>Any other closable resources passed to this function will not be closed by this 067 * implementation. You must handle the lifecycle of those objects yourself. 068 * 069 * @param path the path to add. 070 */ 071 void addPackage(PathRoot path); 072 073 /** 074 * Get a class loader for this group of containers. 075 * 076 * <p>If a class loader has not yet been created, then calling this method is expected to create 077 * a class loader first. 078 * 079 * <p>This method is primarily provided to allow JCT to load components like annotation 080 * processors from provided class paths dynamically during compilation, but is also suitable for 081 * use by users to load classes compiled as part of test cases into memory to perform further 082 * tests on the results via standard reflection APIs. 083 * 084 * <p>While not strictly required, it is recommended that any implementations of this class 085 * provide a subclass of {@link java.net.URLClassLoader} to ensure similar behaviour to the 086 * internals within OpenJDK's {@code javac} implementation. 087 * 088 * <p><strong>Warning</strong>: adding additional containers to this group after accessing this 089 * class loader may result in the class loader being destroyed or re-created. This can result in 090 * confusing behaviour where classes may get loaded multiple times. Generally this shouldn't be an 091 * issue since the class loader is only accessed once the files have been added, but this does 092 * mean that container group types should not be reused between compilation runs if possible. Due 093 * to how the JCT API works, this means that you should avoid calling this method prior to 094 * invoking the compiler itself, and likewise should try to avoid adding new packages to 095 * implementations of container groups after the compiler has been invoked. 096 * 097 * <p><strong>Example of usage with Java:</strong> 098 * 099 * <pre><code> 100 * // Checked exception handling has been omitted from this example for 101 * // brevity. See the java.lang.reflect documentation for full details. 102 * 103 * ClassLoader cl = containerGroup.getClassLoader(); 104 * Class<?> cls = cl.loadClass("org.example.NumberAdder"); 105 * Object adder = cls.getDeclaredConstructor().newInstance(); 106 * Method addMethod = cls.getMethod("add", int.class, int.class); 107 * int result = (int) addMethod.invoke(adder, 9, 18); 108 * 109 * assertThat(result).isEqualTo(27); 110 * </code></pre> 111 * 112 * <p><strong>Example of usage with Groovy:</strong> 113 * 114 * <pre><code class="language-groovy"> 115 * // Groovy is a great option if you are writing lots of tests like this, 116 * // since it will avoid much of the boilerplate around using the reflection 117 * // APIs directly due to the ability to dynamically infer the types of 118 * // objects at runtime. You also avoid having to deal with checked exceptions. 119 * 120 * def cl = containerGroup.getClassLoader() 121 * def cls = cl.loadClass("org.example.NumberAdder") 122 * def adder = cls.getDeclaredConstructor().newInstance() 123 * def result = adder.add(9, 18) 124 * 125 * assertThat(result).isEqualTo(27) 126 * </code></pre> 127 * 128 * <p><strong>Example working with resources:</strong> 129 * 130 * <pre><code> 131 * // Checked exception handling has been omitted from this example for 132 * // brevity. See the java.lang.reflect documentation for full details. 133 * // 134 * // Consider the .getFile method on the PackageContainerGroup class instead 135 * // to achieve the same outcome with simpler syntax. 136 * 137 * ClassLoader cl = containerGroup.getClassLoader(); 138 * try (InputStream inputStream = cl.getResourceAsStream("META-INF/spring.factories")) { 139 * ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 140 * inputStream.transferTo(outputStream); 141 * String content = new String(outputStream.toByteArray(), ...); 142 * ... 143 * } 144 * </code></pre> 145 * 146 * @return a class loader for the contents of this container group. 147 * @see java.lang.ClassLoader 148 * @see java.lang.Class 149 * @see java.lang.reflect.Method 150 * @see java.lang.reflect.Field 151 * @see java.lang.reflect.Constructor 152 * @see java.net.URLClassLoader 153 */ 154 ClassLoader getClassLoader(); 155 156 /** 157 * Find the first occurrence of a given path to a file in packages or modules. 158 * 159 * <p>Paths should be relative to the root of this package group. Absolute paths 160 * will be treated as erroneous inputs. 161 * 162 * <p>Modules are treated as subdirectories where supported. 163 * 164 * <p>This method accepts multiple strings to prevent users from having to 165 * hard-code OS-specific file path separators that may create flaky tests. For example, 166 * {@code .getFile("foo", "bar", "baz")} is equivalent to {@code .getFile("foo/bar/baz")} on most 167 * systems. 168 * 169 * <p>Unlike {@link #getClassLoader}, this will allow access to the files 170 * directly without needing to handle class loading exceptions. 171 * 172 * <pre><code> 173 * // Letting JCT infer the correct path separators to use (recommended). 174 * containerGroup.getFile("foo", "bar", "baz.txt")...; 175 * 176 * // Using platform-specific separators. 177 * containerGroup.getFile("foo/bar/baz.txt")...; 178 * </code></pre> 179 * 180 * @param fragments parts of the path. 181 * @return the first occurrence of the path in this group, or null if not found. 182 * @throws IllegalArgumentException if the provided path is absolute. 183 * @throws IllegalArgumentException if no path fragments are provided. 184 * @see java.nio.file.Path 185 * @see java.nio.file.Files 186 */ 187 @Nullable 188 Path getFile(String... fragments); 189 190 /** 191 * Get a {@link FileObject} that can have content read from it. 192 * 193 * <p>This will return {@code null} if no file is found matching the criteria. 194 * 195 * @param packageName the package name of the file to read. 196 * @param relativeName the relative name of the file to read. 197 * @return the file object, or null if the file is not found. 198 */ 199 @Nullable 200 PathFileObject getFileForInput(String packageName, String relativeName); 201 202 /** 203 * Get a {@link FileObject} that can have content written to it for the given file. 204 * 205 * <p>This will attempt to write to the first writeable path in this group. {@code null} 206 * will be returned if no writeable paths exist in this group. 207 * 208 * @param packageName the name of the package the file is in. 209 * @param relativeName the relative name of the file within the package. 210 * @return the {@link FileObject} to write to, or null if this group has no paths that can be 211 * written to. 212 */ 213 @Nullable 214 PathFileObject getFileForOutput(String packageName, String relativeName); 215 216 /** 217 * Get a {@link JavaFileObject} that can have content read from it for the given file. 218 * 219 * <p>This will return {@code null} if no file is found matching the criteria. 220 * 221 * @param className the binary name of the class to read. 222 * @param kind the kind of file to read. 223 * @return the {@link JavaFileObject} to write to, or null if this group has no paths that can be 224 * written to. 225 */ 226 @Nullable 227 PathFileObject getJavaFileForInput(String className, Kind kind); 228 229 /** 230 * Get a {@link JavaFileObject} that can have content written to it for the given class. 231 * 232 * <p>This will attempt to write to the first writeable path in this group. {@code null} 233 * will be returned if no writeable paths exist in this group. 234 * 235 * @param className the name of the class. 236 * @param kind the kind of the class file. 237 * @return the {@link JavaFileObject} to write to, or null if this group has no paths that can be 238 * written to. 239 */ 240 @Nullable 241 PathFileObject getJavaFileForOutput(String className, Kind kind); 242 243 /** 244 * Get the package-oriented location that this group of paths is for. 245 * 246 * @return the package-oriented location. 247 */ 248 @Override 249 Location getLocation(); 250 251 /** 252 * Get the package containers in this group. 253 * 254 * <p>Returned packages are presented in the order that they were registered. This is the 255 * resolution order that the compiler will use. 256 * 257 * @return the containers. 258 */ 259 List<Container> getPackages(); 260 261 /** 262 * Try to infer the binary name of a given file object. 263 * 264 * @param fileObject the file object to infer the binary name for. 265 * @return the binary name if known, or null otherwise. 266 */ 267 @Nullable 268 String inferBinaryName(PathFileObject fileObject); 269 270 /** 271 * Determine if this group has no paths registered. 272 * 273 * @return {@code true} if no paths are registered. {@code false} if paths are registered. 274 */ 275 boolean isEmpty(); 276 277 /** 278 * List all the file objects that match the given criteria in this group. 279 * 280 * <p>File objects are returned in an unordered collection, but lookup will be 281 * performed in a deterministic order corresponding to the same order as the containers returned 282 * by {@link #getPackages}. 283 * 284 * @param packageName the package name to look in. 285 * @param kinds the kinds of file to look for. 286 * @param recurse {@code true} to recurse subpackages, {@code false} to only consider the 287 * given package. 288 * @return the file objects that were found. 289 * @throws IOException if the file lookup fails due to an IO error somewhere. 290 */ 291 Set<JavaFileObject> listFileObjects( 292 String packageName, 293 Set<? extends Kind> kinds, 294 boolean recurse 295 ) throws IOException; 296 297 /** 298 * List all files recursively in this container group, returning a multimap of each container and 299 * all files within that container. 300 * 301 * @return a multimap of containers mapping to collections of all files in that container. 302 * @throws IOException if the file lookup fails due to an IO error somewhere. 303 * @since 0.6.0 304 */ 305 Map<Container, Collection<Path>> listAllFiles() throws IOException; 306}