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