001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.databind.codegen; 007 008import gov.nist.secauto.metaschema.core.model.IModule; 009import gov.nist.secauto.metaschema.databind.IBindingContext; 010import gov.nist.secauto.metaschema.databind.codegen.config.DefaultBindingConfiguration; 011import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration; 012 013import org.apache.logging.log4j.LogManager; 014import org.apache.logging.log4j.Logger; 015import org.eclipse.jdt.annotation.Owning; 016 017import java.io.IOException; 018import java.lang.module.ModuleDescriptor; 019import java.net.MalformedURLException; 020import java.net.URL; 021import java.net.URLClassLoader; 022import java.nio.file.Path; 023import java.security.AccessController; 024import java.security.PrivilegedAction; 025import java.util.ArrayList; 026import java.util.LinkedList; 027import java.util.List; 028import java.util.stream.Collectors; 029 030import javax.tools.DiagnosticCollector; 031import javax.tools.JavaCompiler; 032import javax.tools.JavaFileManager; 033import javax.tools.JavaFileObject; 034import javax.tools.StandardJavaFileManager; 035import javax.tools.ToolProvider; 036 037import edu.umd.cs.findbugs.annotations.NonNull; 038 039/** 040 * This class provides methods to generate and dynamically compile Java code 041 * based on a Module. The {@link #newClassLoader(Path, ClassLoader)} method can 042 * be used to get a {@link ClassLoader} for Java code previously generated by 043 * this class. 044 */ 045public final class ModuleCompilerHelper { 046 private static final Logger LOGGER = LogManager.getLogger(ModuleCompilerHelper.class); 047 048 private ModuleCompilerHelper() { 049 // disable construction 050 } 051 052 /** 053 * Generate and compile Java class, representing the provided Module 054 * {@code module} and its related definitions, using the default binding 055 * configuration. 056 * 057 * @param module 058 * the Module module to generate Java classes for 059 * @param classDir 060 * the directory to generate the classes in 061 * @return information about the generated classes 062 * @throws IOException 063 * if an error occurred while generating or compiling the classes 064 */ 065 @NonNull 066 public static IProduction compileMetaschema( 067 @NonNull IModule module, 068 @NonNull Path classDir) 069 throws IOException { 070 return compileModule(module, classDir, new DefaultBindingConfiguration()); 071 } 072 073 /** 074 * Generate and compile Java class, representing the provided Module 075 * {@code module} and its related definitions, using the provided custom 076 * {@code bindingConfiguration}. 077 * 078 * @param module 079 * the Module module to generate Java classes for 080 * @param classDir 081 * the directory to generate the classes in 082 * @param bindingConfiguration 083 * configuration settings with directives that tailor the class 084 * generation 085 * @return information about the generated classes 086 * @throws IOException 087 * if an error occurred while generating or compiling the classes 088 */ 089 @NonNull 090 public static IProduction compileModule( 091 @NonNull IModule module, 092 @NonNull Path classDir, 093 @NonNull IBindingConfiguration bindingConfiguration) throws IOException { 094 IProduction production = JavaGenerator.generate(module, classDir, bindingConfiguration); 095 List<IGeneratedClass> classesToCompile = production.getGeneratedClasses().collect(Collectors.toList()); 096 097 DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>(); 098 if (!compileGeneratedClasses(classesToCompile, diagnostics, classDir)) { 099 if (LOGGER.isErrorEnabled()) { 100 LOGGER.error(diagnostics.getDiagnostics().toString()); 101 } 102 throw new IllegalStateException(String.format("failed to compile classes: %s", 103 classesToCompile.stream() 104 .map(clazz -> clazz.getClassName().canonicalName()) 105 .collect(Collectors.joining(",")))); 106 } 107 return production; 108 } 109 110 /** 111 * Create a new classloader capable of loading Java classes generated in the 112 * provided {@code classDir}. 113 * 114 * @param classDir 115 * the directory where generated Java classes have been compiled 116 * @param parent 117 * the classloader to delegate to when the created class loader cannot 118 * load a class 119 * @return the new class loader 120 */ 121 @NonNull 122 public static ClassLoader newClassLoader( 123 @NonNull final Path classDir, 124 @NonNull final ClassLoader parent) { 125 ClassLoader retval = AccessController 126 .doPrivileged((PrivilegedAction<ClassLoader>) () -> newClassLoaderInternal(classDir, parent)); 127 assert retval != null; 128 return retval; 129 } 130 131 @Owning 132 @NonNull 133 private static ClassLoader newClassLoaderInternal( 134 @NonNull final Path classDir, 135 @NonNull final ClassLoader parent) { 136 try { 137 return new URLClassLoader(new URL[] { classDir.toUri().toURL() }, parent); 138 } catch (MalformedURLException ex) { 139 throw new IllegalStateException("unable to configure class loader", ex); 140 } 141 } 142 143 @SuppressWarnings({ 144 "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // acceptable 145 }) 146 private static boolean compile( 147 JavaCompiler compiler, 148 JavaFileManager fileManager, 149 DiagnosticCollector<JavaFileObject> diagnostics, 150 List<JavaFileObject> compilationUnits, 151 Path classDir) { 152 153 String moduleName = null; 154 Module module = IBindingContext.class.getModule(); 155 if (module != null) { 156 ModuleDescriptor descriptor = module.getDescriptor(); 157 if (descriptor != null) { 158 // add the databind module to the task 159 moduleName = descriptor.name(); 160 } 161 } 162 163 List<String> options = new LinkedList<>(); 164 // options.add("-verbose"); 165 // options.add("-g"); 166 options.add("-d"); 167 options.add(classDir.toString()); 168 169 String classPath = System.getProperty("java.class.path"); 170 String modulePath = System.getProperty("jdk.module.path"); 171 if (moduleName == null) { 172 // use classpath only 173 String path = null; 174 if (classPath != null) { 175 path = classPath; 176 } 177 178 if (modulePath != null) { 179 path = path == null ? modulePath : path + ":" + modulePath; 180 } 181 182 if (path != null) { 183 options.add("-classpath"); 184 options.add(path); 185 } 186 } else { 187 // use classpath and modulepath from the JDK 188 if (classPath != null) { 189 options.add("-classpath"); 190 options.add(classPath); 191 } 192 193 if (modulePath != null) { 194 options.add("-p"); 195 options.add(modulePath); 196 } 197 } 198 199 if (LOGGER.isDebugEnabled()) { 200 LOGGER.atDebug().log("Using options: {}", options); 201 } 202 203 JavaCompiler.CompilationTask task 204 = compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits); 205 206 if (moduleName != null) { 207 task.addModules(List.of(moduleName)); 208 } 209 return task.call(); 210 } 211 212 private static boolean compileGeneratedClasses( 213 List<IGeneratedClass> classesToCompile, 214 DiagnosticCollector<JavaFileObject> diagnostics, 215 Path classDir) throws IOException { 216 JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 217 218 try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) { 219 220 List<JavaFileObject> compilationUnits = new ArrayList<>(classesToCompile.size()); 221 for (IGeneratedClass generatedClass : classesToCompile) { 222 compilationUnits.add(fileManager.getJavaFileObjects(generatedClass.getClassFile()).iterator().next()); 223 } 224 225 return compile(compiler, fileManager, diagnostics, compilationUnits, classDir); 226 } 227 } 228}