1
2
3
4
5
6 package gov.nist.secauto.metaschema.maven.plugin;
7
8 import gov.nist.secauto.metaschema.core.metapath.MetapathException;
9 import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
10 import gov.nist.secauto.metaschema.core.model.IModule;
11 import gov.nist.secauto.metaschema.core.model.IModuleLoader;
12 import gov.nist.secauto.metaschema.core.model.IResourceLocation;
13 import gov.nist.secauto.metaschema.core.model.MetaschemaException;
14 import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding;
15 import gov.nist.secauto.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor;
16 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
17 import gov.nist.secauto.metaschema.core.model.validation.AbstractValidationResultProcessor;
18 import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding;
19 import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
20 import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
21 import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
22 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
23 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
24 import gov.nist.secauto.metaschema.databind.DefaultBindingContext;
25 import gov.nist.secauto.metaschema.databind.IBindingContext;
26 import gov.nist.secauto.metaschema.databind.PostProcessingModuleLoaderStrategy;
27 import gov.nist.secauto.metaschema.databind.SimpleModuleLoaderStrategy;
28 import gov.nist.secauto.metaschema.databind.codegen.IGeneratedClass;
29 import gov.nist.secauto.metaschema.databind.codegen.IGeneratedModuleClass;
30 import gov.nist.secauto.metaschema.databind.codegen.IModuleBindingGenerator;
31 import gov.nist.secauto.metaschema.databind.codegen.IProduction;
32 import gov.nist.secauto.metaschema.databind.codegen.JavaCompilerSupport;
33 import gov.nist.secauto.metaschema.databind.codegen.JavaGenerator;
34 import gov.nist.secauto.metaschema.databind.codegen.ModuleCompilerHelper;
35 import gov.nist.secauto.metaschema.databind.codegen.config.DefaultBindingConfiguration;
36 import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration;
37 import gov.nist.secauto.metaschema.databind.model.IBoundModule;
38 import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader;
39 import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
40 import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
41 import gov.nist.secauto.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
42
43 import org.apache.maven.artifact.Artifact;
44 import org.apache.maven.artifact.DependencyResolutionRequiredException;
45 import org.apache.maven.plugin.AbstractMojo;
46 import org.apache.maven.plugin.MojoExecution;
47 import org.apache.maven.plugin.MojoExecutionException;
48 import org.apache.maven.plugin.logging.Log;
49 import org.apache.maven.plugins.annotations.Component;
50 import org.apache.maven.plugins.annotations.Parameter;
51 import org.apache.maven.project.MavenProject;
52 import org.codehaus.plexus.util.DirectoryScanner;
53 import org.sonatype.plexus.build.incremental.BuildContext;
54 import org.xml.sax.SAXParseException;
55
56 import java.io.File;
57 import java.io.IOException;
58 import java.io.OutputStream;
59 import java.net.URI;
60 import java.nio.charset.Charset;
61 import java.nio.file.Files;
62 import java.nio.file.Path;
63 import java.nio.file.Paths;
64 import java.nio.file.StandardOpenOption;
65 import java.util.ArrayList;
66 import java.util.Collection;
67 import java.util.HashSet;
68 import java.util.LinkedHashSet;
69 import java.util.List;
70 import java.util.Objects;
71 import java.util.Set;
72 import java.util.function.Function;
73 import java.util.stream.Collectors;
74 import java.util.stream.Stream;
75
76 import javax.tools.DiagnosticCollector;
77
78 import edu.umd.cs.findbugs.annotations.NonNull;
79 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
80
81 public abstract class AbstractMetaschemaMojo
82 extends AbstractMojo {
83 private static final String[] DEFAULT_INCLUDES = { "**/*.xml" };
84
85
86
87
88
89
90
91 @Parameter(defaultValue = "${project}", required = true, readonly = true)
92 MavenProject mavenProject;
93
94
95
96
97
98
99 @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
110
111
112
113
114
115
116
117
118
119
120 @Parameter(defaultValue = "${project.build.directory}/metaschema", readonly = true, required = true)
121 protected File staleFileDirectory;
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143 @Parameter(defaultValue = "${project.build.sourceEncoding}")
144 private String encoding;
145
146
147
148
149 @Parameter(
150 defaultValue = "${project.build.directory}/generated-sources/metaschema",
151 required = true,
152 property = "outputDirectory")
153 private File outputDirectory;
154
155
156
157
158 @Parameter(defaultValue = "${basedir}/src/main/metaschema")
159 private File metaschemaDir;
160
161
162
163
164 @Parameter(property = "constraints")
165 private File[] constraints;
166
167
168
169
170
171 @Parameter
172 protected String[] includes;
173
174
175
176
177
178 @Parameter
179 protected String[] excludes;
180
181
182
183
184 @Parameter(property = "metaschema.skip", defaultValue = "false")
185 private boolean skip;
186
187
188
189
190
191
192
193
194 protected final BuildContext getBuildContext() {
195 return buildContext;
196 }
197
198
199
200
201
202
203 protected final MavenProject getMavenProject() {
204 return mavenProject;
205 }
206
207 protected final List<Artifact> getPluginArtifacts() {
208 return pluginArtifacts;
209 }
210
211
212
213
214
215
216 @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "this is a data holder")
217 public MojoExecution getMojoExecution() {
218 return mojoExecution;
219 }
220
221
222
223
224
225
226 protected File getOutputDirectory() {
227 return outputDirectory;
228 }
229
230
231
232
233
234
235
236 protected void setOutputDirectory(File outputDirectory) {
237 Objects.requireNonNull(outputDirectory, "outputDirectory");
238 this.outputDirectory = outputDirectory;
239 }
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258 protected final String getEncoding() {
259 String encoding;
260 if (this.encoding != null) {
261
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
277
278
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
296 return new DefaultBindingContext(
297 new PostProcessingModuleLoaderStrategy(
298
299 CollectionUtil.singletonList(modulePostProcessor),
300 new SimpleModuleLoaderStrategy(
301
302
303 new ModuleBindingGenerator(
304 ObjectUtils.notNull(Files.createDirectories(Paths.get("target/metaschema-codegen-modules"))),
305 new DefaultBindingConfiguration()))));
306 }
307
308
309
310
311
312
313
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
331
332
333
334
335 protected boolean shouldExecutionBeSkipped() {
336 return skip;
337 }
338
339
340
341
342
343
344 protected abstract String getStaleFileName();
345
346
347
348
349
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
362
363
364
365
366
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
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
431
432
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
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
514
515
516 createStaleFile(staleFile);
517 }
518
519 if (getLog().isInfoEnabled()) {
520 getLog().info(String.format("Generated %d files.", generatedFiles.size()));
521 }
522
523
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
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
563
564
565
566
567
568
569
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
809
810
811
812 @Override
813 public void processModule(IModule module) {
814 if (!(module instanceof MetaschemaModelModule)) {
815 super.processModule(module);
816 }
817 }
818 }
819 }