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 039 * notes for {@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 processors 083 * from provided class paths dynamically during compilation, but is also suitable for use by users 084 * to load classes compiled as part of test cases into memory to perform further tests on the results 085 * via standard reflection APIs. 086 * 087 * <p>While not strictly required, it is recommended that any implementations of this class provide 088 * a subclass of {@link java.net.URLClassLoader} to ensure similar behaviour to the internals 089 * 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 093 * in confusing behaviour where classes may get loaded multiple times. Generally this shouldn't 094 * be an issue since the class loader is only accessed once the files have been added, but this 095 * does mean that container group types should not be reused between compilation runs if possible. 096 * Due 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 is = cl.getResourceAsStream("META-INF/spring.factories")) { 142 * ByteArrayOutputStream baos = new ByteArrayOutputStream(); 143 * is.transferTo(baos); 144 * String content = new String(baos.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. 169 * For example, {@code .getFile("foo", "bar", "baz")} is equivalent to 170 * {@code .getFile("foo/bar/baz")} on most 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 fragment the first part of the path. 184 * @param fragments any additional parts of the path. 185 * @return the first occurrence of the path in this group, or null if not found. 186 * @throws IllegalArgumentException if the provided path is absolute. 187 * @see java.nio.file.Path 188 * @see java.nio.file.Files 189 */ 190 Path getFile(String fragment, String... fragments); 191 192 /** 193 * Get a {@link FileObject} that can have content read from it. 194 * 195 * <p>This will return {@code null} if no file is found matching the criteria. 196 * 197 * @param packageName the package name of the file to read. 198 * @param relativeName the relative name of the file to read. 199 * @return the file object, or null if the file is not found. 200 */ 201 @Nullable 202 PathFileObject getFileForInput(String packageName, String relativeName); 203 204 /** 205 * Get a {@link FileObject} that can have content written to it for the given file. 206 * 207 * <p>This will attempt to write to the first writeable path in this group. {@code null} 208 * will be returned if no writeable paths exist in this group. 209 * 210 * @param packageName the name of the package the file is in. 211 * @param relativeName the relative name of the file within the package. 212 * @return the {@link FileObject} to write to, or null if this group has no paths that can be 213 * written to. 214 */ 215 @Nullable 216 PathFileObject getFileForOutput(String packageName, String relativeName); 217 218 /** 219 * Get a {@link JavaFileObject} that can have content read from it for the given file. 220 * 221 * <p>This will return {@code null} if no file is found matching the criteria. 222 * 223 * @param className the binary name of the class to read. 224 * @param kind the kind of file to read. 225 * @return the {@link JavaFileObject} to write to, or null if this group has no paths that can be 226 * written to. 227 */ 228 @Nullable 229 PathFileObject getJavaFileForInput(String className, Kind kind); 230 231 /** 232 * Get a {@link JavaFileObject} that can have content written to it for the given class. 233 * 234 * <p>This will attempt to write to the first writeable path in this group. {@code null} 235 * will be returned if no writeable paths exist in this group. 236 * 237 * @param className the name of the class. 238 * @param kind the kind of the class file. 239 * @return the {@link JavaFileObject} to write to, or null if this group has no paths that can be 240 * written to. 241 */ 242 @Nullable 243 PathFileObject getJavaFileForOutput(String className, Kind kind); 244 245 /** 246 * Get the package-oriented location that this group of paths is for. 247 * 248 * @return the package-oriented location. 249 */ 250 Location getLocation(); 251 252 /** 253 * Get the package containers in this group. 254 * 255 * <p>Returned packages are presented in the order that they were registered. This is the 256 * resolution order that the compiler will use. 257 * 258 * @return the containers. 259 */ 260 List<Container> getPackages(); 261 262 /** 263 * Try to infer the binary name of a given file object. 264 * 265 * @param fileObject the file object to infer the binary name for. 266 * @return the binary name if known, or null otherwise. 267 */ 268 @Nullable 269 String inferBinaryName(PathFileObject fileObject); 270 271 /** 272 * Determine if this group has no paths registered. 273 * 274 * @return {@code true} if no paths are registered. {@code false} if paths are registered. 275 */ 276 boolean isEmpty(); 277 278 /** 279 * List all the file objects that match the given criteria in this group. 280 * 281 * <p>File objects are returned in an unordered collection, but lookup will be 282 * performed in a deterministic order corresponding to the same order as the 283 * containers returned by {@link #getPackages}. 284 * 285 * @param packageName the package name to look in. 286 * @param kinds the kinds of file to look for. 287 * @param recurse {@code true} to recurse subpackages, {@code false} to only consider the 288 * given package. 289 * @return the file objects that were found. 290 * @throws IOException if the file lookup fails due to an IO error somewhere. 291 */ 292 Set<JavaFileObject> listFileObjects( 293 String packageName, 294 Set<? extends Kind> kinds, 295 boolean recurse 296 ) throws IOException; 297 298 /** 299 * List all files recursively in this container group, returning a multimap of each container and 300 * all files within that container. 301 * 302 * @return a multimap of containers mapping to collections of all files in that container. 303 * @throws IOException if the file lookup fails due to an IO error somewhere. 304 * @since 0.6.0 305 */ 306 @API(since = "0.6.0", status = Status.STABLE) 307 Map<Container, Collection<Path>> listAllFiles() throws IOException; 308}