1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind.codegen;
7   
8   import gov.nist.secauto.metaschema.core.model.IModule;
9   import gov.nist.secauto.metaschema.core.util.ObjectUtils;
10  import gov.nist.secauto.metaschema.databind.IBindingContext;
11  import gov.nist.secauto.metaschema.databind.codegen.config.DefaultBindingConfiguration;
12  import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration;
13  
14  import org.apache.logging.log4j.LogManager;
15  import org.apache.logging.log4j.Logger;
16  import org.eclipse.jdt.annotation.Owning;
17  
18  import java.io.IOException;
19  import java.lang.module.ModuleDescriptor;
20  import java.net.MalformedURLException;
21  import java.net.URL;
22  import java.net.URLClassLoader;
23  import java.nio.file.Path;
24  import java.security.AccessController;
25  import java.security.PrivilegedAction;
26  import java.util.Arrays;
27  import java.util.List;
28  import java.util.stream.Collectors;
29  
30  import javax.tools.DiagnosticCollector;
31  
32  import edu.umd.cs.findbugs.annotations.NonNull;
33  
34  /**
35   * This class provides methods to generate and dynamically compile Java code
36   * based on a Module. The {@link #newClassLoader(Path, ClassLoader)} method can
37   * be used to get a {@link ClassLoader} for Java code previously generated by
38   * this class.
39   */
40  public final class ModuleCompilerHelper {
41    private static final Logger LOGGER = LogManager.getLogger(ModuleCompilerHelper.class);
42  
43    private ModuleCompilerHelper() {
44      // disable construction
45    }
46  
47    /**
48     * Create a new classloader capable of loading Java classes generated in the
49     * provided {@code classDir}.
50     *
51     * @param classDir
52     *          the directory where generated Java classes have been compiled
53     * @param parent
54     *          the classloader to delegate to when the created class loader cannot
55     *          load a class
56     * @return the new class loader
57     */
58    @SuppressWarnings("resource")
59    @Owning
60    @NonNull
61    public static ClassLoader newClassLoader(
62        @NonNull final Path classDir,
63        @NonNull final ClassLoader parent) {
64      return ObjectUtils.notNull(AccessController.doPrivileged(
65          (PrivilegedAction<ClassLoader>) () -> {
66            try {
67              return new URLClassLoader(new URL[] { classDir.toUri().toURL() }, parent);
68            } catch (MalformedURLException ex) {
69              throw new IllegalStateException("unable to configure class loader", ex);
70            }
71          }));
72    }
73  
74    /**
75     * Generate and compile Java class, representing the provided Module
76     * {@code module} and its related definitions, using the default binding
77     * configuration.
78     *
79     * @param module
80     *          the Module module to generate Java classes for
81     * @param classDir
82     *          the directory to generate the classes in
83     * @return information about the generated classes
84     * @throws IOException
85     *           if an error occurred while generating or compiling the classes
86     */
87    @NonNull
88    public static IProduction compileMetaschema(
89        @NonNull IModule module,
90        @NonNull Path classDir)
91        throws IOException {
92      return compileModule(module, classDir, new DefaultBindingConfiguration());
93    }
94  
95    /**
96     * Generate and compile Java class, representing the provided Module
97     * {@code module} and its related definitions, using the provided custom
98     * {@code bindingConfiguration}.
99     *
100    * @param module
101    *          the Module module to generate Java classes for
102    * @param classDir
103    *          the directory to generate the classes in
104    * @param bindingConfiguration
105    *          configuration settings with directives that tailor the class
106    *          generation
107    * @return information about the generated classes
108    * @throws IOException
109    *           if an error occurred while generating or compiling the classes
110    */
111   @NonNull
112   public static IProduction compileModule(
113       @NonNull IModule module,
114       @NonNull Path classDir,
115       @NonNull IBindingConfiguration bindingConfiguration) throws IOException {
116     IProduction production = JavaGenerator.generate(module, classDir, bindingConfiguration);
117     List<IGeneratedClass> classesToCompile = production.getGeneratedClasses().collect(Collectors.toList());
118 
119     List<Path> classes = ObjectUtils.notNull(classesToCompile.stream()
120         .map(IGeneratedClass::getClassFile)
121         .collect(Collectors.toUnmodifiableList()));
122 
123     // configure the compiler
124     JavaCompilerSupport compiler = new JavaCompilerSupport(classDir);
125     compiler.setLogger(new JavaCompilerSupport.Logger() {
126 
127       @Override
128       public boolean isInfoEnabled() {
129         return LOGGER.isInfoEnabled();
130       }
131 
132       @Override
133       public boolean isDebugEnabled() {
134         return LOGGER.isDebugEnabled();
135       }
136 
137       @Override
138       public void info(String msg) {
139         LOGGER.atInfo().log(msg);
140       }
141 
142       @Override
143       public void debug(String msg) {
144         LOGGER.atDebug().log(msg);
145       }
146     });
147 
148     // determine if we need to use the module path
149     boolean useModulePath = false;
150     Module databindModule = IBindingContext.class.getModule();
151     if (databindModule != null) {
152       ModuleDescriptor descriptor = databindModule.getDescriptor();
153       if (descriptor != null) {
154         // add the databind module to the task
155         compiler.addRootModule(ObjectUtils.notNull(descriptor.name()));
156         useModulePath = true;
157       }
158     }
159 
160     handleClassAndModulePath(compiler, useModulePath);
161 
162     // perform compilation
163     JavaCompilerSupport.CompilationResult result = compiler.compile(classes);
164 
165     if (!result.isSuccessful()) {
166       // log compilation diagnostics
167       DiagnosticCollector<?> diagnostics = new DiagnosticCollector<>();
168       if (LOGGER.isErrorEnabled()) {
169         LOGGER.error(diagnostics.getDiagnostics().toString());
170       }
171       throw new IllegalStateException(String.format("failed to compile classes: %s%nClasspath: %s%nModule Path: %s%n%s",
172           classesToCompile.stream()
173               .map(clazz -> clazz.getClassName().canonicalName())
174               .collect(Collectors.joining(",")),
175           diagnostics.getDiagnostics().toString(),
176           compiler.getClassPath().stream()
177               .collect(Collectors.joining(":")),
178           compiler.getModulePath().stream()
179               .collect(Collectors.joining(":"))));
180     }
181     return production;
182   }
183 
184   private static void handleClassAndModulePath(JavaCompilerSupport compiler, boolean useModulePath) {
185     String classPath = System.getProperty("java.class.path");
186     String modulePath = System.getProperty("jdk.module.path");
187     if (useModulePath) {
188       // use classpath and modulepath from the JDK
189       if (classPath != null) {
190         Arrays.stream(classPath.split(":")).forEachOrdered(compiler::addToClassPath);
191       }
192 
193       if (modulePath != null) {
194         Arrays.stream(modulePath.split(":")).forEachOrdered(compiler::addToModulePath);
195       }
196     } else {
197       // use classpath only
198       if (classPath != null) {
199         Arrays.stream(classPath.split(":")).forEachOrdered(compiler::addToClassPath);
200       }
201 
202       if (modulePath != null) {
203         Arrays.stream(modulePath.split(":")).forEachOrdered(compiler::addToClassPath);
204       }
205     }
206 
207   }
208 }