001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.databind.codegen;
028
029import gov.nist.secauto.metaschema.core.model.IModule;
030import gov.nist.secauto.metaschema.databind.IBindingContext;
031import gov.nist.secauto.metaschema.databind.codegen.config.DefaultBindingConfiguration;
032import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration;
033
034import org.apache.logging.log4j.LogManager;
035import org.apache.logging.log4j.Logger;
036
037import java.io.IOException;
038import java.lang.module.ModuleDescriptor;
039import java.net.MalformedURLException;
040import java.net.URL;
041import java.net.URLClassLoader;
042import java.nio.file.Path;
043import java.security.AccessController;
044import java.security.PrivilegedAction;
045import java.util.ArrayList;
046import java.util.LinkedList;
047import java.util.List;
048import java.util.stream.Collectors;
049
050import javax.tools.DiagnosticCollector;
051import javax.tools.JavaCompiler;
052import javax.tools.JavaFileManager;
053import javax.tools.JavaFileObject;
054import javax.tools.StandardJavaFileManager;
055import javax.tools.ToolProvider;
056
057import edu.umd.cs.findbugs.annotations.NonNull;
058
059/**
060 * This class provides methods to generate and dynamically compile Java code
061 * based on a Module. The {@link #newClassLoader(Path, ClassLoader)} method can
062 * be used to get a {@link ClassLoader} for Java code previously generated by
063 * this class.
064 */
065public final class ModuleCompilerHelper {
066  private static final Logger LOGGER = LogManager.getLogger(ModuleCompilerHelper.class);
067
068  private ModuleCompilerHelper() {
069    // disable construction
070  }
071
072  /**
073   * Generate and compile Java class, representing the provided Module
074   * {@code module} and its related definitions, using the default binding
075   * configuration.
076   *
077   * @param module
078   *          the Module module to generate Java classes for
079   * @param classDir
080   *          the directory to generate the classes in
081   * @return information about the generated classes
082   * @throws IOException
083   *           if an error occurred while generating or compiling the classes
084   */
085  @NonNull
086  public static IProduction compileMetaschema(
087      @NonNull IModule module,
088      @NonNull Path classDir)
089      throws IOException {
090    return compileModule(module, classDir, new DefaultBindingConfiguration());
091  }
092
093  /**
094   * Generate and compile Java class, representing the provided Module
095   * {@code module} and its related definitions, using the provided custom
096   * {@code bindingConfiguration}.
097   *
098   * @param module
099   *          the Module module to generate Java classes for
100   * @param classDir
101   *          the directory to generate the classes in
102   * @param bindingConfiguration
103   *          configuration settings with directives that tailor the class
104   *          generation
105   * @return information about the generated classes
106   * @throws IOException
107   *           if an error occurred while generating or compiling the classes
108   */
109  @NonNull
110  public static IProduction compileModule(
111      @NonNull IModule module,
112      @NonNull Path classDir,
113      @NonNull IBindingConfiguration bindingConfiguration) throws IOException {
114    IProduction production = JavaGenerator.generate(module, classDir, bindingConfiguration);
115    List<IGeneratedClass> classesToCompile = production.getGeneratedClasses().collect(Collectors.toList());
116
117    DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
118    if (!compileGeneratedClasses(classesToCompile, diagnostics, classDir)) {
119      if (LOGGER.isErrorEnabled()) {
120        LOGGER.error(diagnostics.getDiagnostics().toString());
121      }
122      throw new IllegalStateException(String.format("failed to compile classes: %s",
123          classesToCompile.stream()
124              .map(clazz -> clazz.getClassName().canonicalName())
125              .collect(Collectors.joining(","))));
126    }
127    return production;
128  }
129
130  /**
131   * Create a new classloader capable of loading Java classes generated in the
132   * provided {@code classDir}.
133   *
134   * @param classDir
135   *          the directory where generated Java classes have been compiled
136   * @param parent
137   *          the classloader to delegate to when the created class loader cannot
138   *          load a class
139   * @return the new class loader
140   */
141  @SuppressWarnings("null")
142  @NonNull
143  public static ClassLoader newClassLoader(
144      @NonNull final Path classDir,
145      @NonNull final ClassLoader parent) {
146    return AccessController.doPrivileged(new PrivilegedAction<URLClassLoader>() {
147      @Override
148      public URLClassLoader run() {
149        try {
150          return new URLClassLoader(new URL[] { classDir.toUri().toURL() }, parent);
151        } catch (MalformedURLException ex) {
152          throw new IllegalStateException("unable to configure class loader", ex);
153        }
154      }
155    });
156  }
157
158  @SuppressWarnings({
159      "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // acceptable
160  })
161  private static boolean compile(
162      JavaCompiler compiler,
163      JavaFileManager fileManager,
164      DiagnosticCollector<JavaFileObject> diagnostics,
165      List<JavaFileObject> compilationUnits,
166      Path classDir) {
167
168    String moduleName = null;
169    Module module = IBindingContext.class.getModule();
170    if (module != null) {
171      ModuleDescriptor descriptor = module.getDescriptor();
172      if (descriptor != null) {
173        // add the databind module to the task
174        moduleName = descriptor.name();
175      }
176    }
177
178    List<String> options = new LinkedList<>();
179    // options.add("-verbose");
180    // options.add("-g");
181    options.add("-d");
182    options.add(classDir.toString());
183
184    String classPath = System.getProperty("java.class.path");
185    String modulePath = System.getProperty("jdk.module.path");
186    if (moduleName == null) {
187      // use classpath only
188      String path = null;
189      if (classPath != null) {
190        path = classPath;
191      }
192
193      if (modulePath != null) {
194        path = path == null ? modulePath : path + ":" + modulePath;
195      }
196
197      if (path != null) {
198        options.add("-classpath");
199        options.add(path);
200      }
201    } else {
202      // use classpath and modulepath from the JDK
203      if (classPath != null) {
204        options.add("-classpath");
205        options.add(classPath);
206      }
207
208      if (modulePath != null) {
209        options.add("-p");
210        options.add(modulePath);
211      }
212    }
213
214    if (LOGGER.isDebugEnabled()) {
215      LOGGER.atDebug().log("Using options: {}", options);
216    }
217
218    JavaCompiler.CompilationTask task
219        = compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits);
220
221    if (moduleName != null) {
222      task.addModules(List.of(moduleName));
223    }
224    return task.call();
225  }
226
227  private static boolean compileGeneratedClasses(
228      List<IGeneratedClass> classesToCompile,
229      DiagnosticCollector<JavaFileObject> diagnostics,
230      Path classDir) throws IOException {
231    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
232
233    try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) {
234
235      List<JavaFileObject> compilationUnits = new ArrayList<>(classesToCompile.size());
236      for (IGeneratedClass generatedClass : classesToCompile) {
237        compilationUnits.add(fileManager.getJavaFileObjects(generatedClass.getClassFile()).iterator().next());
238      }
239
240      return compile(compiler, fileManager, diagnostics, compilationUnits, classDir);
241    }
242  }
243}