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