1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.maven.plugin;
7   
8   import gov.nist.secauto.metaschema.core.metapath.MetapathException;
9   import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
10  import gov.nist.secauto.metaschema.core.model.IModule;
11  import gov.nist.secauto.metaschema.core.model.IModuleLoader;
12  import gov.nist.secauto.metaschema.core.model.IResourceLocation;
13  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
14  import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding;
15  import gov.nist.secauto.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor;
16  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
17  import gov.nist.secauto.metaschema.core.model.validation.AbstractValidationResultProcessor;
18  import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding;
19  import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
20  import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
21  import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
22  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
23  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
24  import gov.nist.secauto.metaschema.databind.DefaultBindingContext;
25  import gov.nist.secauto.metaschema.databind.IBindingContext;
26  import gov.nist.secauto.metaschema.databind.PostProcessingModuleLoaderStrategy;
27  import gov.nist.secauto.metaschema.databind.SimpleModuleLoaderStrategy;
28  import gov.nist.secauto.metaschema.databind.codegen.IGeneratedClass;
29  import gov.nist.secauto.metaschema.databind.codegen.IGeneratedModuleClass;
30  import gov.nist.secauto.metaschema.databind.codegen.IModuleBindingGenerator;
31  import gov.nist.secauto.metaschema.databind.codegen.IProduction;
32  import gov.nist.secauto.metaschema.databind.codegen.JavaCompilerSupport;
33  import gov.nist.secauto.metaschema.databind.codegen.JavaGenerator;
34  import gov.nist.secauto.metaschema.databind.codegen.ModuleCompilerHelper;
35  import gov.nist.secauto.metaschema.databind.codegen.config.DefaultBindingConfiguration;
36  import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration;
37  import gov.nist.secauto.metaschema.databind.model.IBoundModule;
38  import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader;
39  import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
40  import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
41  import gov.nist.secauto.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
42  
43  import org.apache.maven.artifact.Artifact;
44  import org.apache.maven.artifact.DependencyResolutionRequiredException;
45  import org.apache.maven.plugin.AbstractMojo;
46  import org.apache.maven.plugin.MojoExecution;
47  import org.apache.maven.plugin.MojoExecutionException;
48  import org.apache.maven.plugin.logging.Log;
49  import org.apache.maven.plugins.annotations.Component;
50  import org.apache.maven.plugins.annotations.Parameter;
51  import org.apache.maven.project.MavenProject;
52  import org.codehaus.plexus.util.DirectoryScanner;
53  import org.sonatype.plexus.build.incremental.BuildContext;
54  import org.xml.sax.SAXParseException;
55  
56  import java.io.File;
57  import java.io.IOException;
58  import java.io.OutputStream;
59  import java.net.URI;
60  import java.nio.charset.Charset;
61  import java.nio.file.Files;
62  import java.nio.file.Path;
63  import java.nio.file.Paths;
64  import java.nio.file.StandardOpenOption;
65  import java.util.ArrayList;
66  import java.util.Collection;
67  import java.util.HashSet;
68  import java.util.LinkedHashSet;
69  import java.util.List;
70  import java.util.Objects;
71  import java.util.Set;
72  import java.util.function.Function;
73  import java.util.stream.Collectors;
74  import java.util.stream.Stream;
75  
76  import javax.tools.DiagnosticCollector;
77  
78  import edu.umd.cs.findbugs.annotations.NonNull;
79  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
80  
81  public abstract class AbstractMetaschemaMojo
82      extends AbstractMojo {
83    private static final String[] DEFAULT_INCLUDES = { "**/*.xml" };
84  
85    /**
86     * The Maven project context.
87     *
88     * @required
89     * @readonly
90     */
91    @Parameter(defaultValue = "${project}", required = true, readonly = true)
92    MavenProject mavenProject;
93  
94    /**
95     * This will be injected if this plugin is executed as part of the standard
96     * Maven lifecycle. If the mojo is directly invoked, this parameter will not be
97     * injected.
98     */
99    @Parameter(defaultValue = "${mojoExecution}", readonly = true)
100   private MojoExecution mojoExecution;
101 
102   @Component
103   private BuildContext buildContext;
104 
105   @Parameter(defaultValue = "${plugin.artifacts}", readonly = true, required = true)
106   private List<Artifact> pluginArtifacts;
107 
108   /**
109    * <p>
110    * The directory where the staleFile is found. The staleFile is used to
111    * determine if re-generation of generated Java classes is needed, by recording
112    * when the last build occurred.
113    * </p>
114    * <p>
115    * This directory is expected to be located within the
116    * <code>${project.build.directory}</code>, to ensure that code (re)generation
117    * occurs after cleaning the project.
118    * </p>
119    */
120   @Parameter(defaultValue = "${project.build.directory}/metaschema", readonly = true, required = true)
121   protected File staleFileDirectory;
122 
123   /**
124    * <p>
125    * Defines the encoding used for generating Java Source files.
126    * </p>
127    * <p>
128    * The algorithm for finding the encoding to use is as follows (where the first
129    * non-null value found is used for encoding):
130    * <ol>
131    * <li>If the configuration property is explicitly given within the plugin's
132    * configuration, use that value.
133    * <li>If the Maven property <code>project.build.sourceEncoding</code> is
134    * defined, use its value.
135    * <li>Otherwise use the value from the system property
136    * <code>file.encoding</code>.
137    * </ol>
138    * </p>
139    *
140    * @see #getEncoding()
141    * @since 2.0
142    */
143   @Parameter(defaultValue = "${project.build.sourceEncoding}")
144   private String encoding;
145 
146   /**
147    * Location to generate Java source files in.
148    */
149   @Parameter(
150       defaultValue = "${project.build.directory}/generated-sources/metaschema",
151       required = true,
152       property = "outputDirectory")
153   private File outputDirectory;
154 
155   /**
156    * The directory to read source metaschema from.
157    */
158   @Parameter(defaultValue = "${basedir}/src/main/metaschema")
159   private File metaschemaDir;
160 
161   /**
162    * A list of <code>files</code> containing Metaschema module constraints files.
163    */
164   @Parameter(property = "constraints")
165   private File[] constraints;
166 
167   /**
168    * A set of inclusion patterns used to select which Metaschema modules are to be
169    * processed. By default, all files are processed.
170    */
171   @Parameter
172   protected String[] includes;
173 
174   /**
175    * A set of exclusion patterns used to prevent certain files from being
176    * processed. By default, this set is empty such that no files are excluded.
177    */
178   @Parameter
179   protected String[] excludes;
180 
181   /**
182    * Indicate if the execution should be skipped.
183    */
184   @Parameter(property = "metaschema.skip", defaultValue = "false")
185   private boolean skip;
186 
187   /**
188    * The BuildContext is used to identify which files or directories were modified
189    * since last build. This is used to determine if Module-based generation must
190    * be performed again.
191    *
192    * @return the active Plexus BuildContext.
193    */
194   protected final BuildContext getBuildContext() {
195     return buildContext;
196   }
197 
198   /**
199    * Retrieve the Maven project context.
200    *
201    * @return The active MavenProject.
202    */
203   protected final MavenProject getMavenProject() {
204     return mavenProject;
205   }
206 
207   protected final List<Artifact> getPluginArtifacts() {
208     return pluginArtifacts;
209   }
210 
211   /**
212    * Retrieve the mojo execution context.
213    *
214    * @return The active MojoExecution.
215    */
216   @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "this is a data holder")
217   public MojoExecution getMojoExecution() {
218     return mojoExecution;
219   }
220 
221   /**
222    * Retrieve the directory where generated classes will be stored.
223    *
224    * @return the directory
225    */
226   protected File getOutputDirectory() {
227     return outputDirectory;
228   }
229 
230   /**
231    * Set the directory where generated classes will be stored.
232    *
233    * @param outputDirectory
234    *          the directory to use
235    */
236   protected void setOutputDirectory(File outputDirectory) {
237     Objects.requireNonNull(outputDirectory, "outputDirectory");
238     this.outputDirectory = outputDirectory;
239   }
240 
241   /**
242    * Gets the file encoding to use for generated classes.
243    * <p>
244    * The algorithm for finding the encoding to use is as follows (where the first
245    * non-null value found is used for encoding):
246    * </p>
247    * <ol>
248    * <li>If the configuration property is explicitly given within the plugin's
249    * configuration, use that value.
250    * <li>If the Maven property <code>project.build.sourceEncoding</code> is
251    * defined, use its value.
252    * <li>Otherwise use the value from the system property
253    * <code>file.encoding</code>.
254    * </ol>
255    *
256    * @return The encoding to be used by this AbstractJaxbMojo and its tools.
257    */
258   protected final String getEncoding() {
259     String encoding;
260     if (this.encoding != null) {
261       // first try to use the provided encoding
262       encoding = this.encoding;
263       if (getLog().isDebugEnabled()) {
264         getLog().debug(String.format("Using configured encoding [%s].", encoding));
265       }
266     } else {
267       encoding = Charset.defaultCharset().displayName();
268       if (getLog().isWarnEnabled()) {
269         getLog().warn(String.format("Using system encoding [%s]. This build is platform dependent!", encoding));
270       }
271     }
272     return encoding;
273   }
274 
275   /**
276    * Retrieve a stream of Module file sources.
277    *
278    * @return the stream
279    */
280   protected Stream<File> getModuleSources() {
281     DirectoryScanner ds = new DirectoryScanner();
282     ds.setBasedir(metaschemaDir);
283     ds.setIncludes(includes != null && includes.length > 0 ? includes : DEFAULT_INCLUDES);
284     ds.setExcludes(excludes != null && excludes.length > 0 ? excludes : null);
285     ds.addDefaultExcludes();
286     ds.setCaseSensitive(true);
287     ds.setFollowSymlinks(false);
288     ds.scan();
289     return Stream.of(ds.getIncludedFiles()).map(filename -> new File(metaschemaDir, filename)).distinct();
290   }
291 
292   @NonNull
293   protected IBindingContext newBindingContext(
294       @NonNull IModuleLoader.IModulePostProcessor modulePostProcessor) throws IOException, MetaschemaException {
295     // generate Java sources based on provided metaschema sources
296     return new DefaultBindingContext(
297         new PostProcessingModuleLoaderStrategy(
298             // ensure that the external constraints do not apply to the built in module
299             CollectionUtil.singletonList(modulePostProcessor),
300             new SimpleModuleLoaderStrategy(
301                 // this is used instead of the default generator to ensure that plugin classpath
302                 // entries are used for compilation
303                 new ModuleBindingGenerator(
304                     ObjectUtils.notNull(Files.createDirectories(Paths.get("target/metaschema-codegen-modules"))),
305                     new DefaultBindingConfiguration()))));
306   }
307 
308   /**
309    * Get the configured collection of constraints.
310    *
311    * @return the loaded constraints
312    * @throws MojoExecutionException
313    *           if an error occurred while loading the constraints
314    */
315   @NonNull
316   protected List<IConstraintSet> getConstraints() throws MojoExecutionException {
317     IConstraintLoader loader = IBindingContext.getConstraintLoader();
318     List<IConstraintSet> constraintSets = new ArrayList<>(constraints.length);
319     for (File constraint : this.constraints) {
320       try {
321         constraintSets.addAll(loader.load(ObjectUtils.notNull(constraint)));
322       } catch (IOException | MetaschemaException ex) {
323         throw new MojoExecutionException("Loading of external constraints failed", ex);
324       }
325     }
326     return CollectionUtil.unmodifiableList(constraintSets);
327   }
328 
329   /**
330    * Determine if the execution of this mojo should be skipped.
331    *
332    * @return {@code true} if the mojo execution should be skipped, or
333    *         {@code false} otherwise
334    */
335   protected boolean shouldExecutionBeSkipped() {
336     return skip;
337   }
338 
339   /**
340    * Get the name of the file that is used to detect staleness.
341    *
342    * @return the name
343    */
344   protected abstract String getStaleFileName();
345 
346   /**
347    * Gets the staleFile for this execution.
348    *
349    * @return the staleFile
350    */
351   protected final File getStaleFile() {
352     StringBuilder builder = new StringBuilder();
353     if (getMojoExecution() != null) {
354       builder.append(getMojoExecution().getExecutionId()).append('-');
355     }
356     builder.append(getStaleFileName());
357     return new File(staleFileDirectory, builder.toString());
358   }
359 
360   /**
361    * Determine if code generation is required. This is done by comparing the last
362    * modified time of each Module source file against the stale file managed by
363    * this plugin.
364    *
365    * @return {@code true} if the code generation is needed, or {@code false}
366    *         otherwise
367    */
368   protected boolean isGenerationRequired() {
369     final File staleFile = getStaleFile();
370     boolean generate = !staleFile.exists();
371     if (generate) {
372       if (getLog().isInfoEnabled()) {
373         getLog().info(String.format("Stale file '%s' doesn't exist! Generating source files.", staleFile.getPath()));
374       }
375       generate = true;
376     } else {
377       generate = false;
378       // check for staleness
379       long staleLastModified = staleFile.lastModified();
380 
381       BuildContext buildContext = getBuildContext();
382       URI metaschemaDirRelative = getMavenProject().getBasedir().toURI().relativize(metaschemaDir.toURI());
383 
384       if (buildContext.isIncremental() && buildContext.hasDelta(metaschemaDirRelative.toString())) {
385         if (getLog().isInfoEnabled()) {
386           getLog().info("metaschemaDirRelative: " + metaschemaDirRelative.toString());
387         }
388         generate = true;
389       }
390 
391       if (!generate) {
392         for (File sourceFile : getModuleSources().collect(Collectors.toList())) {
393           if (getLog().isInfoEnabled()) {
394             getLog().info("Source file: " + sourceFile.getPath());
395           }
396           if (sourceFile.lastModified() > staleLastModified) {
397             generate = true;
398           }
399         }
400       }
401     }
402     return generate;
403   }
404 
405   protected Set<String> getClassPath() throws DependencyResolutionRequiredException {
406     Set<String> pathElements;
407     try {
408       pathElements = new LinkedHashSet<>(getMavenProject().getCompileClasspathElements());
409     } catch (DependencyResolutionRequiredException ex) {
410       getLog().warn("exception calling getCompileClasspathElements", ex);
411       throw ex;
412     }
413 
414     if (pluginArtifacts != null) {
415       for (Artifact a : getPluginArtifacts()) {
416         if (a.getFile() != null) {
417           pathElements.add(a.getFile().getAbsolutePath());
418         }
419       }
420     }
421     return pathElements;
422   }
423 
424   @NonNull
425   protected Set<IModule> getModulesToGenerateFor(
426       @NonNull IBindingContext bindingContext,
427       @NonNull IModuleLoader.IModulePostProcessor modulePostProcessor)
428       throws MetaschemaException, IOException {
429 
430     // Don't use the normal loader, since it attempts to register and compile the
431     // module.
432     // We only care about the module content for generating sources and schemas
433     IBindingModuleLoader loader = new BindingModuleLoader(bindingContext, (module, ctx) -> {
434       modulePostProcessor.processModule(module);
435     });
436     loader.allowEntityResolution();
437 
438     LoggingValidationHandler validationHandler = new LoggingValidationHandler();
439 
440     Set<IModule> modules = new HashSet<>();
441     for (File source : getModuleSources().collect(Collectors.toList())) {
442       assert source != null;
443       if (getLog().isInfoEnabled()) {
444         getLog().info("Using metaschema source: " + source.getPath());
445       }
446       IBindingMetaschemaModule module = loader.load(source);
447 
448       IValidationResult result = bindingContext.validate(
449           module.getSourceNodeItem(),
450           loader.getBindingContext().newBoundLoader(),
451           null);
452 
453       validationHandler.handleResults(result);
454 
455       modules.add(module);
456     }
457     return modules;
458   }
459 
460   protected void createStaleFile(@NonNull File staleFile) throws MojoExecutionException {
461     // create the stale file
462     if (!staleFileDirectory.exists() && !staleFileDirectory.mkdirs()) {
463       throw new MojoExecutionException("Unable to create output directory: " + staleFileDirectory);
464     }
465     try (OutputStream os
466         = Files.newOutputStream(staleFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE,
467             StandardOpenOption.TRUNCATE_EXISTING)) {
468       os.close();
469       if (getLog().isInfoEnabled()) {
470         getLog().info("Created stale file: " + staleFile);
471       }
472     } catch (IOException ex) {
473       throw new MojoExecutionException("Failed to write stale file: " + staleFile.getPath(), ex);
474     }
475   }
476 
477   @SuppressWarnings("PMD.AvoidCatchingGenericException")
478   @Override
479   public void execute() throws MojoExecutionException {
480     File staleFile = getStaleFile();
481     try {
482       staleFile = ObjectUtils.notNull(staleFile.getCanonicalFile());
483     } catch (IOException ex) {
484       if (getLog().isWarnEnabled()) {
485         getLog().warn("Unable to resolve canonical path to stale file. Treating it as not existing.", ex);
486       }
487     }
488 
489     boolean generate;
490     if (shouldExecutionBeSkipped()) {
491       if (getLog().isDebugEnabled()) {
492         getLog().debug(String.format("Generation is configured to be skipped. Skipping."));
493       }
494       generate = false;
495     } else if (staleFile.exists()) {
496       generate = isGenerationRequired();
497     } else {
498       if (getLog().isInfoEnabled()) {
499         getLog().info(String.format("Stale file '%s' doesn't exist! Generation is required.", staleFile.getPath()));
500       }
501       generate = true;
502     }
503 
504     if (generate) {
505       List<IConstraintSet> constraints = getConstraints();
506       IModuleLoader.IModulePostProcessor modulePostProcessor
507           = new LimitedExternalConstraintsModulePostProcessor(constraints);
508 
509       List<File> generatedFiles;
510       try {
511         generatedFiles = performGeneration(modulePostProcessor);
512       } finally {
513         // ensure the stale file is created to ensure that regeneration is only
514         // performed when a
515         // change is made
516         createStaleFile(staleFile);
517       }
518 
519       if (getLog().isInfoEnabled()) {
520         getLog().info(String.format("Generated %d files.", generatedFiles.size()));
521       }
522 
523       // for m2e
524       for (File file : generatedFiles) {
525         getBuildContext().refresh(file);
526       }
527     }
528   }
529 
530   @SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.ExceptionAsFlowControl" })
531   @NonNull
532   private List<File> performGeneration(
533       @NonNull IModuleLoader.IModulePostProcessor modulePostProcessor) throws MojoExecutionException {
534     File outputDir = getOutputDirectory();
535     if (getLog().isDebugEnabled()) {
536       getLog().debug(String.format("Using outputDirectory: %s", outputDir.getPath()));
537     }
538 
539     if (!outputDir.exists() && !outputDir.mkdirs()) {
540       throw new MojoExecutionException("Unable to create output directory: " + outputDir);
541     }
542 
543     IBindingContext bindingContext;
544     try {
545       bindingContext = newBindingContext(modulePostProcessor);
546     } catch (MetaschemaException | IOException ex) {
547       throw new MojoExecutionException("Failed to create the binding context", ex);
548     }
549 
550     // generate Java sources based on provided metaschema sources
551     Set<IModule> modules;
552     try {
553       modules = getModulesToGenerateFor(bindingContext, modulePostProcessor);
554     } catch (Exception ex) {
555       throw new MojoExecutionException("Loading of metaschema modules failed", ex);
556     }
557 
558     return generate(modules);
559   }
560 
561   /**
562    * Perform the generation operation.
563    *
564    * @param modules
565    *          the modules to generate resources/sources for
566    *
567    * @return the files generated during the operation
568    * @throws MojoExecutionException
569    *           if an error occurred while performing the generation operation
570    */
571   @NonNull
572   protected abstract List<File> generate(@NonNull Set<IModule> modules) throws MojoExecutionException;
573 
574   protected final class LoggingValidationHandler
575       extends AbstractValidationResultProcessor {
576 
577     private <T extends IValidationFinding> void handleFinding(
578         @NonNull T finding,
579         @NonNull Function<T, CharSequence> formatter) {
580 
581       Log log = getLog();
582 
583       switch (finding.getSeverity()) {
584       case CRITICAL:
585       case ERROR:
586         if (log.isErrorEnabled()) {
587           log.error(formatter.apply(finding), finding.getCause());
588         }
589         break;
590       case WARNING:
591         if (log.isWarnEnabled()) {
592           getLog().warn(formatter.apply(finding), finding.getCause());
593         }
594         break;
595       case INFORMATIONAL:
596         if (log.isInfoEnabled()) {
597           getLog().info(formatter.apply(finding), finding.getCause());
598         }
599         break;
600       default:
601         if (log.isDebugEnabled()) {
602           getLog().debug(formatter.apply(finding), finding.getCause());
603         }
604         break;
605       }
606     }
607 
608     @Override
609     protected void handleJsonValidationFinding(JsonValidationFinding finding) {
610       handleFinding(finding, this::getMessage);
611     }
612 
613     @Override
614     protected void handleXmlValidationFinding(XmlValidationFinding finding) {
615       handleFinding(finding, this::getMessage);
616     }
617 
618     @Override
619     protected void handleConstraintValidationFinding(ConstraintValidationFinding finding) {
620       handleFinding(finding, this::getMessage);
621     }
622 
623     @NonNull
624     private CharSequence getMessage(JsonValidationFinding finding) {
625       StringBuilder builder = new StringBuilder();
626       builder.append('[')
627           .append(finding.getCause().getPointerToViolation())
628           .append("] ")
629           .append(finding.getMessage());
630 
631       URI documentUri = finding.getDocumentUri();
632       if (documentUri != null) {
633         builder.append(" [")
634             .append(documentUri.toString())
635             .append(']');
636       }
637       return builder;
638     }
639 
640     @NonNull
641     private CharSequence getMessage(XmlValidationFinding finding) {
642       StringBuilder builder = new StringBuilder();
643 
644       builder.append(finding.getMessage())
645           .append(" [");
646 
647       URI documentUri = finding.getDocumentUri();
648       if (documentUri != null) {
649         builder.append(documentUri.toString());
650       }
651 
652       SAXParseException ex = finding.getCause();
653       builder.append(finding.getMessage())
654           .append('{')
655           .append(ex.getLineNumber())
656           .append(',')
657           .append(ex.getColumnNumber())
658           .append("}]");
659       return builder;
660     }
661 
662     @NonNull
663     private CharSequence getMessage(@NonNull ConstraintValidationFinding finding) {
664       StringBuilder builder = new StringBuilder();
665       builder.append('[')
666           .append(finding.getTarget().getMetapath())
667           .append(']');
668 
669       String id = finding.getIdentifier();
670       if (id != null) {
671         builder.append(' ')
672             .append(id);
673       }
674 
675       builder.append(' ')
676           .append(finding.getMessage());
677 
678       URI documentUri = finding.getTarget().getBaseUri();
679       IResourceLocation location = finding.getLocation();
680       if (documentUri != null || location != null) {
681         builder.append(" [");
682       }
683 
684       if (documentUri != null) {
685         builder.append(documentUri.toString());
686       }
687 
688       if (location != null) {
689         builder.append('{')
690             .append(location.getLine())
691             .append(',')
692             .append(location.getColumn())
693             .append('}');
694       }
695       if (documentUri != null || location != null) {
696         builder.append(']');
697       }
698       return builder;
699     }
700   }
701 
702   public class ModuleBindingGenerator implements IModuleBindingGenerator {
703     @NonNull
704     private final Path compilePath;
705     @NonNull
706     private final ClassLoader classLoader;
707     @NonNull
708     private final IBindingConfiguration bindingConfiguration;
709 
710     public ModuleBindingGenerator(
711         @NonNull Path compilePath,
712         @NonNull IBindingConfiguration bindingConfiguration) {
713       this.compilePath = compilePath;
714       this.classLoader = ModuleCompilerHelper.newClassLoader(
715           compilePath,
716           ObjectUtils.notNull(Thread.currentThread().getContextClassLoader()));
717       this.bindingConfiguration = bindingConfiguration;
718     }
719 
720     @NonNull
721     public IProduction generateClasses(@NonNull IModule module) {
722       IProduction production;
723       try {
724         production = JavaGenerator.generate(module, compilePath, bindingConfiguration);
725       } catch (IOException ex) {
726         throw new MetapathException(
727             String.format("Unable to generate and compile classes for module '%s'.", module.getLocation()),
728             ex);
729       }
730       return production;
731     }
732 
733     private void compileClasses(@NonNull IProduction production, @NonNull Path classDir)
734         throws IOException, DependencyResolutionRequiredException {
735       List<IGeneratedClass> classesToCompile = production.getGeneratedClasses().collect(Collectors.toList());
736 
737       List<Path> classes = ObjectUtils.notNull(classesToCompile.stream()
738           .map(IGeneratedClass::getClassFile)
739           .collect(Collectors.toUnmodifiableList()));
740 
741       JavaCompilerSupport compiler = new JavaCompilerSupport(classDir);
742       compiler.setLogger(new JavaCompilerSupport.Logger() {
743 
744         @Override
745         public boolean isDebugEnabled() {
746           return getLog().isDebugEnabled();
747         }
748 
749         @Override
750         public boolean isInfoEnabled() {
751           return getLog().isInfoEnabled();
752         }
753 
754         @Override
755         public void debug(String msg) {
756           getLog().debug(msg);
757         }
758 
759         @Override
760         public void info(String msg) {
761           getLog().info(msg);
762         }
763       });
764 
765       getClassPath().forEach(compiler::addToClassPath);
766 
767       JavaCompilerSupport.CompilationResult result = compiler.compile(classes);
768 
769       if (!result.isSuccessful()) {
770         DiagnosticCollector<?> diagnostics = new DiagnosticCollector<>();
771         if (getLog().isErrorEnabled()) {
772           getLog().error("diagnostics: " + diagnostics.getDiagnostics().toString());
773         }
774         throw new IllegalStateException(String.format("failed to compile classes: %s",
775             classesToCompile.stream()
776                 .map(clazz -> clazz.getClassName().canonicalName())
777                 .collect(Collectors.joining(","))));
778       }
779     }
780 
781     @Override
782     public Class<? extends IBoundModule> generate(IModule module) {
783       IProduction production = generateClasses(module);
784       try {
785         compileClasses(production, compilePath);
786       } catch (IOException | DependencyResolutionRequiredException ex) {
787         throw new IllegalStateException("failed to compile classes", ex);
788       }
789       IGeneratedModuleClass moduleClass = ObjectUtils.requireNonNull(production.getModuleProduction(module));
790 
791       try {
792         return moduleClass.load(classLoader);
793       } catch (ClassNotFoundException ex) {
794         throw new IllegalStateException(ex);
795       }
796     }
797   }
798 
799   private static class LimitedExternalConstraintsModulePostProcessor
800       extends ExternalConstraintsModulePostProcessor {
801 
802     public LimitedExternalConstraintsModulePostProcessor(
803         @NonNull Collection<IConstraintSet> additionalConstraintSets) {
804       super(additionalConstraintSets);
805     }
806 
807     /**
808      * This method ensures that constraints are not applied to the built-in
809      * Metaschema module module twice, when this module is selected as the source
810      * for generation.
811      */
812     @Override
813     public void processModule(IModule module) {
814       if (!(module instanceof MetaschemaModelModule)) {
815         super.processModule(module);
816       }
817     }
818   }
819 }