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