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&lt;?&gt; 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}