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}