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}