001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.maven.plugin;
007
008import org.apache.maven.artifact.Artifact;
009import org.apache.maven.artifact.DependencyResolutionRequiredException;
010import org.apache.maven.plugin.AbstractMojo;
011import org.apache.maven.plugin.MojoExecution;
012import org.apache.maven.plugin.MojoExecutionException;
013import org.apache.maven.plugin.logging.Log;
014import org.apache.maven.plugins.annotations.Parameter;
015import org.apache.maven.project.MavenProject;
016import org.codehaus.plexus.util.DirectoryScanner;
017import org.sonatype.plexus.build.incremental.BuildContext;
018import org.xml.sax.SAXParseException;
019
020import java.io.File;
021import java.io.IOException;
022import java.io.OutputStream;
023import java.net.URI;
024import java.nio.charset.Charset;
025import java.nio.file.Files;
026import java.nio.file.Path;
027import java.nio.file.Paths;
028import java.nio.file.StandardOpenOption;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.HashSet;
032import java.util.LinkedHashSet;
033import java.util.List;
034import java.util.Objects;
035import java.util.Set;
036import java.util.function.Function;
037import java.util.stream.Collectors;
038import java.util.stream.Stream;
039
040import javax.inject.Inject;
041import javax.tools.DiagnosticCollector;
042
043import dev.metaschema.core.model.IConstraintLoader;
044import dev.metaschema.core.model.IModule;
045import dev.metaschema.core.model.IModuleLoader;
046import dev.metaschema.core.model.IResourceLocation;
047import dev.metaschema.core.model.MetaschemaException;
048import dev.metaschema.core.model.constraint.ConstraintValidationException;
049import dev.metaschema.core.model.constraint.ConstraintValidationFinding;
050import dev.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor;
051import dev.metaschema.core.model.constraint.IConstraintSet;
052import dev.metaschema.core.model.validation.AbstractValidationResultProcessor;
053import dev.metaschema.core.model.validation.IValidationFinding;
054import dev.metaschema.core.model.validation.IValidationResult;
055import dev.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
056import dev.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
057import dev.metaschema.core.util.CollectionUtil;
058import dev.metaschema.core.util.ObjectUtils;
059import dev.metaschema.databind.DefaultBindingContext;
060import dev.metaschema.databind.IBindingContext;
061import dev.metaschema.databind.PostProcessingModuleLoaderStrategy;
062import dev.metaschema.databind.SimpleModuleLoaderStrategy;
063import dev.metaschema.databind.codegen.IGeneratedClass;
064import dev.metaschema.databind.codegen.IGeneratedModuleClass;
065import dev.metaschema.databind.codegen.IModuleBindingGenerator;
066import dev.metaschema.databind.codegen.IProduction;
067import dev.metaschema.databind.codegen.JavaCompilerSupport;
068import dev.metaschema.databind.codegen.JavaGenerator;
069import dev.metaschema.databind.codegen.ModuleCompilerHelper;
070import dev.metaschema.databind.codegen.config.DefaultBindingConfiguration;
071import dev.metaschema.databind.codegen.config.IBindingConfiguration;
072import dev.metaschema.databind.model.IBoundModule;
073import dev.metaschema.databind.model.metaschema.BindingModuleLoader;
074import dev.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
075import dev.metaschema.databind.model.metaschema.IBindingModuleLoader;
076import dev.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
077import edu.umd.cs.findbugs.annotations.NonNull;
078import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
079
080/**
081 * Abstract base class for Metaschema Maven plugin goals.
082 * <p>
083 * This class provides common functionality for loading Metaschema modules,
084 * managing constraint sets, handling incremental builds, and performing
085 * code/schema generation. Concrete implementations should override the
086 * {@link #generate(Set)} method to provide specific generation behavior.
087 * <p>
088 * The plugin supports:
089 * <ul>
090 * <li>Loading multiple Metaschema modules from a configured directory</li>
091 * <li>Applying external constraint sets to modules</li>
092 * <li>Incremental build support through stale file tracking</li>
093 * <li>Configurable file encoding for generated sources</li>
094 * </ul>
095 *
096 * @see GenerateSourcesMojo
097 * @see GenerateSchemaMojo
098 */
099public 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}