001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.codegen;
007
008import java.io.IOException;
009import java.io.StringWriter;
010import java.nio.file.Path;
011import java.util.LinkedHashSet;
012import java.util.LinkedList;
013import java.util.List;
014import java.util.Set;
015import java.util.stream.Collectors;
016
017import javax.tools.DiagnosticCollector;
018import javax.tools.JavaCompiler;
019import javax.tools.JavaFileObject;
020import javax.tools.StandardJavaFileManager;
021import javax.tools.ToolProvider;
022
023import dev.metaschema.core.util.CollectionUtil;
024import edu.umd.cs.findbugs.annotations.NonNull;
025import edu.umd.cs.findbugs.annotations.Nullable;
026
027/**
028 * Provides support for compiling Java source files using the system Java
029 * compiler.
030 * <p>
031 * This class wraps the {@link javax.tools.JavaCompiler} API to provide a
032 * simplified interface for compiling generated Java source files. It supports
033 * configuring the classpath, module path, and output directory.
034 */
035public class JavaCompilerSupport {
036  @Nullable
037  private Logger logger;
038  @NonNull
039  private final Path classDir;
040  @NonNull
041  private final Set<String> classPath = new LinkedHashSet<>();
042  @NonNull
043  private final Set<String> modulePath = new LinkedHashSet<>();
044  @NonNull
045  private final Set<String> rootModuleNames = new LinkedHashSet<>();
046
047  /**
048   * Construct a new compiler support instance.
049   *
050   * @param classDir
051   *          the directory where compiled class files will be written
052   */
053  public JavaCompilerSupport(@NonNull Path classDir) {
054    this.classDir = classDir;
055  }
056
057  /**
058   * Get the configured classpath entries.
059   *
060   * @return the classpath entries
061   */
062  public Set<String> getClassPath() {
063    return classPath;
064  }
065
066  /**
067   * Get the configured module path entries.
068   *
069   * @return the module path entries
070   */
071  public Set<String> getModulePath() {
072    return modulePath;
073  }
074
075  /**
076   * Get the configured root module names.
077   *
078   * @return the root module names
079   */
080  public Set<String> getRootModuleNames() {
081    return rootModuleNames;
082  }
083
084  /**
085   * Add an entry to the classpath.
086   *
087   * @param entry
088   *          the classpath entry to add
089   */
090  public void addToClassPath(@NonNull String entry) {
091    classPath.add(entry);
092  }
093
094  /**
095   * Add an entry to the module path.
096   *
097   * @param entry
098   *          the module path entry to add
099   */
100  public void addToModulePath(@NonNull String entry) {
101    modulePath.add(entry);
102  }
103
104  /**
105   * Add a root module name.
106   *
107   * @param entry
108   *          the root module name to add
109   */
110  public void addRootModule(@NonNull String entry) {
111    rootModuleNames.add(entry);
112  }
113
114  /**
115   * Set the logger for compilation messages.
116   *
117   * @param logger
118   *          the logger to use
119   */
120  public void setLogger(@NonNull Logger logger) {
121    this.logger = logger;
122  }
123
124  /**
125   * Generate the compiler options based on the current configuration.
126   *
127   * @return the list of compiler options
128   */
129  @NonNull
130  protected List<String> generateCompilerOptions() {
131    List<String> options = new LinkedList<>();
132    options.add("-d");
133    options.add(classDir.toString());
134
135    if (!classPath.isEmpty()) {
136      options.add("-classpath");
137      options.add(classPath.stream()
138          .collect(Collectors.joining(":")));
139    }
140
141    if (!modulePath.isEmpty()) {
142      options.add("-p");
143      options.add(modulePath.stream()
144          .collect(Collectors.joining(":")));
145    }
146
147    return options;
148  }
149
150  /**
151   * Compile the provided Java source files.
152   *
153   * @param classFiles
154   *          the source files to compile
155   * @return information about the compilation result
156   * @throws IOException
157   *           if an error occurred while compiling the classes
158   * @throws IllegalArgumentException
159   *           if any of the options are invalid, or if any of the given
160   *           compilation units are of other kind than
161   *           {@link javax.tools.JavaFileObject.Kind#SOURCE}
162   */
163  public CompilationResult compile(@NonNull List<Path> classFiles) throws IOException {
164    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
165
166    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
167
168    List<JavaFileObject> compilationUnits;
169    try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) {
170
171      compilationUnits = classFiles.stream()
172          .map(fileManager::getJavaFileObjects)
173          .map(CollectionUtil::toList)
174          .flatMap(List::stream)
175          .collect(Collectors.toUnmodifiableList());
176
177      List<String> options = generateCompilerOptions();
178
179      Logger logger = this.logger;
180      if (logger != null && logger.isDebugEnabled()) {
181        logger.debug(String.format("Using options: %s", options));
182      }
183
184      boolean result;
185      try (StringWriter writer = new StringWriter()) {
186        JavaCompiler.CompilationTask task = compiler.getTask(
187            writer,
188            fileManager,
189            diagnostics,
190            options,
191            null,
192            compilationUnits);
193        task.addModules(rootModuleNames);
194
195        result = task.call();
196        writer.flush();
197        String output = writer.toString();
198        if (!output.isBlank() && logger != null && logger.isInfoEnabled()) {
199          logger.info(String.format("compiler output: %s", writer.toString()));
200        }
201      }
202      return new CompilationResult(result, diagnostics);
203    }
204  }
205
206  /**
207   * Contains the result of a compilation operation.
208   */
209  public static final class CompilationResult {
210    private final boolean successful;
211    @NonNull
212    private final DiagnosticCollector<JavaFileObject> diagnostics;
213
214    private CompilationResult(boolean successful, @NonNull DiagnosticCollector<JavaFileObject> diagnostics) {
215      this.successful = successful;
216      this.diagnostics = diagnostics;
217    }
218
219    /**
220     * Check if the compilation was successful.
221     *
222     * @return {@code true} if compilation succeeded, {@code false} otherwise
223     */
224    public boolean isSuccessful() {
225      return successful;
226    }
227
228    /**
229     * Get the compilation diagnostics.
230     *
231     * @return the diagnostics collector containing any warnings or errors
232     */
233    public DiagnosticCollector<?> getDiagnostics() {
234      return diagnostics;
235    }
236  }
237
238  /**
239   * A logging interface for compilation messages.
240   */
241  public interface Logger {
242    /**
243     * Check if debug logging is enabled.
244     *
245     * @return {@code true} if debug logging is enabled
246     */
247    boolean isDebugEnabled();
248
249    /**
250     * Check if info logging is enabled.
251     *
252     * @return {@code true} if info logging is enabled
253     */
254    boolean isInfoEnabled();
255
256    /**
257     * Log a debug message.
258     *
259     * @param msg
260     *          the message to log
261     */
262    void debug(String msg);
263
264    /**
265     * Log an info message.
266     *
267     * @param msg
268     *          the message to log
269     */
270    void info(String msg);
271  }
272}