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