View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.metaschema.databind.codegen;
28  
29  import gov.nist.secauto.metaschema.core.model.IModule;
30  import gov.nist.secauto.metaschema.databind.IBindingContext;
31  import gov.nist.secauto.metaschema.databind.codegen.config.DefaultBindingConfiguration;
32  import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration;
33  
34  import org.apache.logging.log4j.LogManager;
35  import org.apache.logging.log4j.Logger;
36  
37  import java.io.IOException;
38  import java.lang.module.ModuleDescriptor;
39  import java.net.MalformedURLException;
40  import java.net.URL;
41  import java.net.URLClassLoader;
42  import java.nio.file.Path;
43  import java.security.AccessController;
44  import java.security.PrivilegedAction;
45  import java.util.ArrayList;
46  import java.util.LinkedList;
47  import java.util.List;
48  import java.util.stream.Collectors;
49  
50  import javax.tools.DiagnosticCollector;
51  import javax.tools.JavaCompiler;
52  import javax.tools.JavaFileManager;
53  import javax.tools.JavaFileObject;
54  import javax.tools.StandardJavaFileManager;
55  import javax.tools.ToolProvider;
56  
57  import edu.umd.cs.findbugs.annotations.NonNull;
58  
59  /**
60   * This class provides methods to generate and dynamically compile Java code
61   * based on a Module. The {@link #newClassLoader(Path, ClassLoader)} method can
62   * be used to get a {@link ClassLoader} for Java code previously generated by
63   * this class.
64   */
65  public final class ModuleCompilerHelper {
66    private static final Logger LOGGER = LogManager.getLogger(ModuleCompilerHelper.class);
67  
68    private ModuleCompilerHelper() {
69      // disable construction
70    }
71  
72    /**
73     * Generate and compile Java class, representing the provided Module
74     * {@code module} and its related definitions, using the default binding
75     * configuration.
76     *
77     * @param module
78     *          the Module module to generate Java classes for
79     * @param classDir
80     *          the directory to generate the classes in
81     * @return information about the generated classes
82     * @throws IOException
83     *           if an error occurred while generating or compiling the classes
84     */
85    @NonNull
86    public static IProduction compileMetaschema(
87        @NonNull IModule module,
88        @NonNull Path classDir)
89        throws IOException {
90      return compileModule(module, classDir, new DefaultBindingConfiguration());
91    }
92  
93    /**
94     * Generate and compile Java class, representing the provided Module
95     * {@code module} and its related definitions, using the provided custom
96     * {@code bindingConfiguration}.
97     *
98     * @param module
99     *          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 }