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