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