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}