ModuleCompilerHelper.java
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/
package gov.nist.secauto.metaschema.databind.codegen;
import gov.nist.secauto.metaschema.core.model.IModule;
import gov.nist.secauto.metaschema.databind.IBindingContext;
import gov.nist.secauto.metaschema.databind.codegen.config.DefaultBindingConfiguration;
import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jdt.annotation.Owning;
import java.io.IOException;
import java.lang.module.ModuleDescriptor;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import edu.umd.cs.findbugs.annotations.NonNull;
/**
* This class provides methods to generate and dynamically compile Java code
* based on a Module. The {@link #newClassLoader(Path, ClassLoader)} method can
* be used to get a {@link ClassLoader} for Java code previously generated by
* this class.
*/
public final class ModuleCompilerHelper {
private static final Logger LOGGER = LogManager.getLogger(ModuleCompilerHelper.class);
private ModuleCompilerHelper() {
// disable construction
}
/**
* Generate and compile Java class, representing the provided Module
* {@code module} and its related definitions, using the default binding
* configuration.
*
* @param module
* the Module module to generate Java classes for
* @param classDir
* the directory to generate the classes in
* @return information about the generated classes
* @throws IOException
* if an error occurred while generating or compiling the classes
*/
@NonNull
public static IProduction compileMetaschema(
@NonNull IModule module,
@NonNull Path classDir)
throws IOException {
return compileModule(module, classDir, new DefaultBindingConfiguration());
}
/**
* Generate and compile Java class, representing the provided Module
* {@code module} and its related definitions, using the provided custom
* {@code bindingConfiguration}.
*
* @param module
* the Module module to generate Java classes for
* @param classDir
* the directory to generate the classes in
* @param bindingConfiguration
* configuration settings with directives that tailor the class
* generation
* @return information about the generated classes
* @throws IOException
* if an error occurred while generating or compiling the classes
*/
@NonNull
public static IProduction compileModule(
@NonNull IModule module,
@NonNull Path classDir,
@NonNull IBindingConfiguration bindingConfiguration) throws IOException {
IProduction production = JavaGenerator.generate(module, classDir, bindingConfiguration);
List<IGeneratedClass> classesToCompile = production.getGeneratedClasses().collect(Collectors.toList());
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
if (!compileGeneratedClasses(classesToCompile, diagnostics, classDir)) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error(diagnostics.getDiagnostics().toString());
}
throw new IllegalStateException(String.format("failed to compile classes: %s",
classesToCompile.stream()
.map(clazz -> clazz.getClassName().canonicalName())
.collect(Collectors.joining(","))));
}
return production;
}
/**
* Create a new classloader capable of loading Java classes generated in the
* provided {@code classDir}.
*
* @param classDir
* the directory where generated Java classes have been compiled
* @param parent
* the classloader to delegate to when the created class loader cannot
* load a class
* @return the new class loader
*/
@NonNull
public static ClassLoader newClassLoader(
@NonNull final Path classDir,
@NonNull final ClassLoader parent) {
ClassLoader retval = AccessController
.doPrivileged((PrivilegedAction<ClassLoader>) () -> newClassLoaderInternal(classDir, parent));
assert retval != null;
return retval;
}
@Owning
@NonNull
private static ClassLoader newClassLoaderInternal(
@NonNull final Path classDir,
@NonNull final ClassLoader parent) {
try {
return new URLClassLoader(new URL[] { classDir.toUri().toURL() }, parent);
} catch (MalformedURLException ex) {
throw new IllegalStateException("unable to configure class loader", ex);
}
}
@SuppressWarnings({
"PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // acceptable
})
private static boolean compile(
JavaCompiler compiler,
JavaFileManager fileManager,
DiagnosticCollector<JavaFileObject> diagnostics,
List<JavaFileObject> compilationUnits,
Path classDir) {
String moduleName = null;
Module module = IBindingContext.class.getModule();
if (module != null) {
ModuleDescriptor descriptor = module.getDescriptor();
if (descriptor != null) {
// add the databind module to the task
moduleName = descriptor.name();
}
}
List<String> options = new LinkedList<>();
// options.add("-verbose");
// options.add("-g");
options.add("-d");
options.add(classDir.toString());
String classPath = System.getProperty("java.class.path");
String modulePath = System.getProperty("jdk.module.path");
if (moduleName == null) {
// use classpath only
String path = null;
if (classPath != null) {
path = classPath;
}
if (modulePath != null) {
path = path == null ? modulePath : path + ":" + modulePath;
}
if (path != null) {
options.add("-classpath");
options.add(path);
}
} else {
// use classpath and modulepath from the JDK
if (classPath != null) {
options.add("-classpath");
options.add(classPath);
}
if (modulePath != null) {
options.add("-p");
options.add(modulePath);
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.atDebug().log("Using options: {}", options);
}
JavaCompiler.CompilationTask task
= compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits);
if (moduleName != null) {
task.addModules(List.of(moduleName));
}
return task.call();
}
private static boolean compileGeneratedClasses(
List<IGeneratedClass> classesToCompile,
DiagnosticCollector<JavaFileObject> diagnostics,
Path classDir) throws IOException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) {
List<JavaFileObject> compilationUnits = new ArrayList<>(classesToCompile.size());
for (IGeneratedClass generatedClass : classesToCompile) {
compilationUnits.add(fileManager.getJavaFileObjects(generatedClass.getClassFile()).iterator().next());
}
return compile(compiler, fileManager, diagnostics, compilationUnits, classDir);
}
}
}