1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.databind.codegen;
7   
8   import org.apache.logging.log4j.LogManager;
9   import org.apache.logging.log4j.Logger;
10  import org.eclipse.jdt.annotation.Owning;
11  
12  import java.io.IOException;
13  import java.lang.module.ModuleDescriptor;
14  import java.net.MalformedURLException;
15  import java.net.URL;
16  import java.net.URLClassLoader;
17  import java.nio.file.Path;
18  import java.security.AccessController;
19  import java.security.PrivilegedAction;
20  import java.util.Arrays;
21  import java.util.List;
22  import java.util.stream.Collectors;
23  
24  import javax.tools.DiagnosticCollector;
25  
26  import dev.metaschema.core.model.IModule;
27  import dev.metaschema.core.util.ObjectUtils;
28  import dev.metaschema.databind.IBindingContext;
29  import dev.metaschema.databind.codegen.config.DefaultBindingConfiguration;
30  import dev.metaschema.databind.codegen.config.IBindingConfiguration;
31  import edu.umd.cs.findbugs.annotations.NonNull;
32  
33  /**
34   * This class provides methods to generate and dynamically compile Java code
35   * based on a Module. The {@link #newClassLoader(Path, ClassLoader)} method can
36   * be used to get a {@link ClassLoader} for Java code previously generated by
37   * this class.
38   */
39  public final class ModuleCompilerHelper {
40    private static final Logger LOGGER = LogManager.getLogger(ModuleCompilerHelper.class);
41  
42    private ModuleCompilerHelper() {
43      // disable construction
44    }
45  
46    /**
47     * Create a new classloader capable of loading Java classes generated in the
48     * provided {@code classDir}.
49     * <p>
50     * The caller owns the returned class loader and is responsible for closing it.
51     *
52     * @param classDir
53     *          the directory where generated Java classes have been compiled
54     * @param parent
55     *          the classloader to delegate to when the created class loader cannot
56     *          load a class
57     * @return the new class loader
58     */
59    @SuppressWarnings("resource")
60    @Owning
61    @NonNull
62    public static ClassLoader newClassLoader(
63        @NonNull final Path classDir,
64        @NonNull final ClassLoader parent) {
65      return ObjectUtils.notNull(AccessController.doPrivileged(
66          (PrivilegedAction<ClassLoader>) () -> {
67            try {
68              return new URLClassLoader(new URL[] { classDir.toUri().toURL() }, parent);
69            } catch (MalformedURLException ex) {
70              throw new IllegalStateException("unable to configure class loader", ex);
71            }
72          }));
73    }
74  
75    /**
76     * Generate and compile Java class, representing the provided Module
77     * {@code module} and its related definitions, using the default binding
78     * configuration.
79     *
80     * @param module
81     *          the Module module to generate Java classes for
82     * @param classDir
83     *          the directory to generate the classes in
84     * @return information about the generated classes
85     * @throws IOException
86     *           if an error occurred while generating or compiling the classes
87     */
88    @NonNull
89    public static IProduction compileMetaschema(
90        @NonNull IModule module,
91        @NonNull Path classDir)
92        throws IOException {
93      return compileModule(module, classDir, new DefaultBindingConfiguration());
94    }
95  
96    /**
97     * Generate and compile Java class, representing the provided Module
98     * {@code module} and its related definitions, using the provided custom
99     * {@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 }