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