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.core.util.ObjectUtils;
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.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import javax.tools.DiagnosticCollector;

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
  }

  /**
   * 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
   */
  @SuppressWarnings("resource")
  @Owning
  @NonNull
  public static ClassLoader newClassLoader(
      @NonNull final Path classDir,
      @NonNull final ClassLoader parent) {
    return ObjectUtils.notNull(AccessController.doPrivileged(
        (PrivilegedAction<ClassLoader>) () -> {
          try {
            return new URLClassLoader(new URL[] { classDir.toUri().toURL() }, parent);
          } catch (MalformedURLException ex) {
            throw new IllegalStateException("unable to configure class loader", ex);
          }
        }));
  }

  /**
   * 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());

    List<Path> classes = ObjectUtils.notNull(classesToCompile.stream()
        .map(IGeneratedClass::getClassFile)
        .collect(Collectors.toUnmodifiableList()));

    JavaCompilerSupport compiler = new JavaCompilerSupport(classDir);

    boolean usingModule = false;
    Module databindModule = IBindingContext.class.getModule();
    if (databindModule != null) {
      ModuleDescriptor descriptor = databindModule.getDescriptor();
      if (descriptor != null) {
        // add the databind module to the task
        compiler.addRootModule(ObjectUtils.notNull(descriptor.name()));
        usingModule = true;
      }
    }

    String classPath = System.getProperty("java.class.path");
    String modulePath = System.getProperty("jdk.module.path");
    if (usingModule) {
      // use classpath and modulepath from the JDK
      if (classPath != null) {
        Arrays.stream(classPath.split(":")).forEachOrdered(compiler::addToClassPath);
      }

      if (modulePath != null) {
        Arrays.stream(modulePath.split(":")).forEachOrdered(compiler::addToModulePath);
      }
    } else {
      // use classpath only
      if (classPath != null) {
        Arrays.stream(classPath.split(":")).forEachOrdered(compiler::addToClassPath);
      }

      if (modulePath != null) {
        Arrays.stream(modulePath.split(":")).forEachOrdered(compiler::addToClassPath);
      }
    }

    JavaCompilerSupport.CompilationResult result = compiler.compile(classes, null);

    if (!result.isSuccessful()) {
      DiagnosticCollector<?> diagnostics = new DiagnosticCollector<>();
      if (LOGGER.isErrorEnabled()) {
        LOGGER.error(diagnostics.getDiagnostics().toString());
      }
      throw new IllegalStateException(String.format("failed to compile classes: %s%nClasspath: %s%nModule Path: %s%n%s",
          classesToCompile.stream()
              .map(clazz -> clazz.getClassName().canonicalName())
              .collect(Collectors.joining(",")),
          diagnostics.getDiagnostics().toString(),
          compiler.getClassPath().stream()
              .collect(Collectors.joining(":")),
          compiler.getModulePath().stream()
              .collect(Collectors.joining(":"))));
    }
    return production;
  }
}