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}