1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.codegen;
7   
8   import java.io.IOException;
9   import java.io.StringWriter;
10  import java.nio.file.Path;
11  import java.util.LinkedHashSet;
12  import java.util.LinkedList;
13  import java.util.List;
14  import java.util.Set;
15  import java.util.stream.Collectors;
16  
17  import javax.tools.DiagnosticCollector;
18  import javax.tools.JavaCompiler;
19  import javax.tools.JavaFileObject;
20  import javax.tools.StandardJavaFileManager;
21  import javax.tools.ToolProvider;
22  
23  import dev.metaschema.core.util.CollectionUtil;
24  import edu.umd.cs.findbugs.annotations.NonNull;
25  import edu.umd.cs.findbugs.annotations.Nullable;
26  
27  /**
28   * Provides support for compiling Java source files using the system Java
29   * compiler.
30   * <p>
31   * This class wraps the {@link javax.tools.JavaCompiler} API to provide a
32   * simplified interface for compiling generated Java source files. It supports
33   * configuring the classpath, module path, and output directory.
34   */
35  public class JavaCompilerSupport {
36    @Nullable
37    private Logger logger;
38    @NonNull
39    private final Path classDir;
40    @NonNull
41    private final Set<String> classPath = new LinkedHashSet<>();
42    @NonNull
43    private final Set<String> modulePath = new LinkedHashSet<>();
44    @NonNull
45    private final Set<String> rootModuleNames = new LinkedHashSet<>();
46  
47    /**
48     * Construct a new compiler support instance.
49     *
50     * @param classDir
51     *          the directory where compiled class files will be written
52     */
53    public JavaCompilerSupport(@NonNull Path classDir) {
54      this.classDir = classDir;
55    }
56  
57    /**
58     * Get the configured classpath entries.
59     *
60     * @return the classpath entries
61     */
62    public Set<String> getClassPath() {
63      return classPath;
64    }
65  
66    /**
67     * Get the configured module path entries.
68     *
69     * @return the module path entries
70     */
71    public Set<String> getModulePath() {
72      return modulePath;
73    }
74  
75    /**
76     * Get the configured root module names.
77     *
78     * @return the root module names
79     */
80    public Set<String> getRootModuleNames() {
81      return rootModuleNames;
82    }
83  
84    /**
85     * Add an entry to the classpath.
86     *
87     * @param entry
88     *          the classpath entry to add
89     */
90    public void addToClassPath(@NonNull String entry) {
91      classPath.add(entry);
92    }
93  
94    /**
95     * Add an entry to the module path.
96     *
97     * @param entry
98     *          the module path entry to add
99     */
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 }