1
2
3
4
5
6 package gov.nist.secauto.metaschema.maven.plugin;
7
8 import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
9 import gov.nist.secauto.metaschema.core.model.IModule;
10 import gov.nist.secauto.metaschema.core.model.IModuleLoader;
11 import gov.nist.secauto.metaschema.core.model.IResourceLocation;
12 import gov.nist.secauto.metaschema.core.model.MetaschemaException;
13 import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationException;
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.Parameter;
50
51 import javax.inject.Inject;
52 import org.apache.maven.project.MavenProject;
53 import org.codehaus.plexus.util.DirectoryScanner;
54 import org.sonatype.plexus.build.incremental.BuildContext;
55 import org.xml.sax.SAXParseException;
56
57 import java.io.File;
58 import java.io.IOException;
59 import java.io.OutputStream;
60 import java.net.URI;
61 import java.nio.charset.Charset;
62 import java.nio.file.Files;
63 import java.nio.file.Path;
64 import java.nio.file.Paths;
65 import java.nio.file.StandardOpenOption;
66 import java.util.ArrayList;
67 import java.util.Collection;
68 import java.util.HashSet;
69 import java.util.LinkedHashSet;
70 import java.util.List;
71 import java.util.Objects;
72 import java.util.Set;
73 import java.util.function.Function;
74 import java.util.stream.Collectors;
75 import java.util.stream.Stream;
76
77 import javax.tools.DiagnosticCollector;
78
79 import edu.umd.cs.findbugs.annotations.NonNull;
80 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
81
82 public abstract class AbstractMetaschemaMojo
83 extends AbstractMojo {
84 private static final String[] DEFAULT_INCLUDES = { "**/*.xml" };
85
86
87
88
89
90
91
92 @Parameter(defaultValue = "${project}", required = true, readonly = true)
93 MavenProject mavenProject;
94
95
96
97
98
99
100 @Parameter(defaultValue = "${mojoExecution}", readonly = true)
101 private MojoExecution mojoExecution;
102
103 @Inject
104 private BuildContext buildContext;
105
106 @Parameter(defaultValue = "${plugin.artifacts}", readonly = true, required = true)
107 private List<Artifact> pluginArtifacts;
108
109
110
111
112
113
114
115
116
117
118
119
120
121 @Parameter(defaultValue = "${project.build.directory}/metaschema", readonly = true, required = true)
122 protected File staleFileDirectory;
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144 @Parameter(defaultValue = "${project.build.sourceEncoding}")
145 private String encoding;
146
147
148
149
150 @Parameter(
151 defaultValue = "${project.build.directory}/generated-sources/metaschema",
152 required = true,
153 property = "outputDirectory")
154 private File outputDirectory;
155
156
157
158
159 @Parameter(defaultValue = "${basedir}/src/main/metaschema")
160 private File metaschemaDir;
161
162
163
164
165 @Parameter(property = "constraints")
166 private File[] constraints;
167
168
169
170
171
172 @Parameter
173 protected String[] includes;
174
175
176
177
178
179 @Parameter
180 protected String[] excludes;
181
182
183
184
185 @Parameter(property = "metaschema.skip", defaultValue = "false")
186 private boolean skip;
187
188
189
190
191
192
193
194
195 protected final BuildContext getBuildContext() {
196 return buildContext;
197 }
198
199
200
201
202
203
204 protected final MavenProject getMavenProject() {
205 return mavenProject;
206 }
207
208 protected final List<Artifact> getPluginArtifacts() {
209 return pluginArtifacts;
210 }
211
212
213
214
215
216
217 @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "this is a data holder")
218 public MojoExecution getMojoExecution() {
219 return mojoExecution;
220 }
221
222
223
224
225
226
227 protected File getOutputDirectory() {
228 return outputDirectory;
229 }
230
231
232
233
234
235
236
237 protected void setOutputDirectory(File outputDirectory) {
238 Objects.requireNonNull(outputDirectory, "outputDirectory");
239 this.outputDirectory = outputDirectory;
240 }
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259 protected final String getEncoding() {
260 String encoding;
261 if (this.encoding != null) {
262
263 encoding = this.encoding;
264 if (getLog().isDebugEnabled()) {
265 getLog().debug(String.format("Using configured encoding [%s].", encoding));
266 }
267 } else {
268 encoding = Charset.defaultCharset().displayName();
269 if (getLog().isWarnEnabled()) {
270 getLog().warn(String.format("Using system encoding [%s]. This build is platform dependent!", encoding));
271 }
272 }
273 return encoding;
274 }
275
276
277
278
279
280
281 protected Stream<File> getModuleSources() {
282 DirectoryScanner ds = new DirectoryScanner();
283 ds.setBasedir(metaschemaDir);
284 ds.setIncludes(includes != null && includes.length > 0 ? includes : DEFAULT_INCLUDES);
285 ds.setExcludes(excludes != null && excludes.length > 0 ? excludes : null);
286 ds.addDefaultExcludes();
287 ds.setCaseSensitive(true);
288 ds.setFollowSymlinks(false);
289 ds.scan();
290 return Stream.of(ds.getIncludedFiles()).map(filename -> new File(metaschemaDir, filename)).distinct();
291 }
292
293 @NonNull
294 protected IBindingContext newBindingContext(
295 @NonNull IModuleLoader.IModulePostProcessor modulePostProcessor) throws IOException, MetaschemaException {
296
297 return new DefaultBindingContext(
298 new PostProcessingModuleLoaderStrategy(
299
300 CollectionUtil.singletonList(modulePostProcessor),
301 new SimpleModuleLoaderStrategy(
302
303
304 new ModuleBindingGenerator(
305 ObjectUtils.notNull(Files.createDirectories(Paths.get("target/metaschema-codegen-modules"))),
306 new DefaultBindingConfiguration()))));
307 }
308
309
310
311
312
313
314
315
316 @NonNull
317 protected List<IConstraintSet> getConstraints() throws MojoExecutionException {
318 IConstraintLoader loader = IBindingContext.getConstraintLoader();
319 List<IConstraintSet> constraintSets = new ArrayList<>(constraints.length);
320 for (File constraint : this.constraints) {
321 try {
322 constraintSets.addAll(loader.load(ObjectUtils.notNull(constraint)));
323 } catch (IOException | MetaschemaException ex) {
324 throw new MojoExecutionException("Loading of external constraints failed", ex);
325 }
326 }
327 return CollectionUtil.unmodifiableList(constraintSets);
328 }
329
330
331
332
333
334
335
336 protected boolean shouldExecutionBeSkipped() {
337 return skip;
338 }
339
340
341
342
343
344
345 protected abstract String getStaleFileName();
346
347
348
349
350
351
352 protected final File getStaleFile() {
353 StringBuilder builder = new StringBuilder();
354 if (getMojoExecution() != null) {
355 builder.append(getMojoExecution().getExecutionId()).append('-');
356 }
357 builder.append(getStaleFileName());
358 return new File(staleFileDirectory, builder.toString());
359 }
360
361
362
363
364
365
366
367
368
369 protected boolean isGenerationRequired() {
370 final File staleFile = getStaleFile();
371 boolean generate = !staleFile.exists();
372 if (generate) {
373 if (getLog().isInfoEnabled()) {
374 getLog().info(String.format("Stale file '%s' doesn't exist! Generating source files.", staleFile.getPath()));
375 }
376 generate = true;
377 } else {
378 generate = false;
379
380 long staleLastModified = staleFile.lastModified();
381
382 BuildContext buildContext = getBuildContext();
383 URI metaschemaDirRelative = getMavenProject().getBasedir().toURI().relativize(metaschemaDir.toURI());
384
385 if (buildContext.isIncremental() && buildContext.hasDelta(metaschemaDirRelative.toString())) {
386 if (getLog().isInfoEnabled()) {
387 getLog().info("metaschemaDirRelative: " + metaschemaDirRelative.toString());
388 }
389 generate = true;
390 }
391
392 if (!generate) {
393 for (File sourceFile : getModuleSources().collect(Collectors.toList())) {
394 if (getLog().isInfoEnabled()) {
395 getLog().info("Source file: " + sourceFile.getPath());
396 }
397 if (sourceFile.lastModified() > staleLastModified) {
398 generate = true;
399 }
400 }
401 }
402 }
403 return generate;
404 }
405
406 protected Set<String> getClassPath() throws DependencyResolutionRequiredException {
407 Set<String> pathElements;
408 try {
409 pathElements = new LinkedHashSet<>(getMavenProject().getCompileClasspathElements());
410 } catch (DependencyResolutionRequiredException ex) {
411 getLog().warn("exception calling getCompileClasspathElements", ex);
412 throw ex;
413 }
414
415 if (pluginArtifacts != null) {
416 for (Artifact a : getPluginArtifacts()) {
417 if (a.getFile() != null) {
418 pathElements.add(a.getFile().getAbsolutePath());
419 }
420 }
421 }
422 return pathElements;
423 }
424
425 @NonNull
426 protected Set<IModule> getModulesToGenerateFor(
427 @NonNull IBindingContext bindingContext,
428 @NonNull IModuleLoader.IModulePostProcessor modulePostProcessor)
429 throws MetaschemaException, IOException, ConstraintValidationException {
430
431
432
433
434 IBindingModuleLoader loader = new BindingModuleLoader(bindingContext, (module, ctx) -> {
435 modulePostProcessor.processModule(module);
436 });
437 loader.allowEntityResolution();
438
439 LoggingValidationHandler validationHandler = new LoggingValidationHandler();
440
441 Set<IModule> modules = new HashSet<>();
442 for (File source : getModuleSources().collect(Collectors.toList())) {
443 assert source != null;
444 if (getLog().isInfoEnabled()) {
445 getLog().info("Using metaschema source: " + source.getPath());
446 }
447 IBindingMetaschemaModule module = loader.load(source);
448
449 IValidationResult result = bindingContext.validate(
450 module.getSourceNodeItem(),
451 loader.getBindingContext().newBoundLoader(),
452 null);
453
454 validationHandler.handleResults(result);
455
456 modules.add(module);
457 }
458 return modules;
459 }
460
461 protected void createStaleFile(@NonNull File staleFile) throws MojoExecutionException {
462
463 if (!staleFileDirectory.exists() && !staleFileDirectory.mkdirs()) {
464 throw new MojoExecutionException("Unable to create output directory: " + staleFileDirectory);
465 }
466 try (OutputStream os
467 = Files.newOutputStream(staleFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE,
468 StandardOpenOption.TRUNCATE_EXISTING)) {
469 os.close();
470 if (getLog().isInfoEnabled()) {
471 getLog().info("Created stale file: " + staleFile);
472 }
473 } catch (IOException ex) {
474 throw new MojoExecutionException("Failed to write stale file: " + staleFile.getPath(), ex);
475 }
476 }
477
478 @SuppressWarnings("PMD.AvoidCatchingGenericException")
479 @Override
480 public void execute() throws MojoExecutionException {
481 File staleFile = getStaleFile();
482 try {
483 staleFile = ObjectUtils.notNull(staleFile.getCanonicalFile());
484 } catch (IOException ex) {
485 if (getLog().isWarnEnabled()) {
486 getLog().warn("Unable to resolve canonical path to stale file. Treating it as not existing.", ex);
487 }
488 }
489
490 boolean generate;
491 if (shouldExecutionBeSkipped()) {
492 if (getLog().isDebugEnabled()) {
493 getLog().debug(String.format("Generation is configured to be skipped. Skipping."));
494 }
495 generate = false;
496 } else if (staleFile.exists()) {
497 generate = isGenerationRequired();
498 } else {
499 if (getLog().isInfoEnabled()) {
500 getLog().info(String.format("Stale file '%s' doesn't exist! Generation is required.", staleFile.getPath()));
501 }
502 generate = true;
503 }
504
505 if (generate) {
506 List<IConstraintSet> constraints = getConstraints();
507 IModuleLoader.IModulePostProcessor modulePostProcessor
508 = new LimitedExternalConstraintsModulePostProcessor(constraints);
509
510 List<File> generatedFiles;
511 try {
512 generatedFiles = performGeneration(modulePostProcessor);
513 } finally {
514
515
516
517 createStaleFile(staleFile);
518 }
519
520 if (getLog().isInfoEnabled()) {
521 getLog().info(String.format("Generated %d files.", generatedFiles.size()));
522 }
523
524
525 for (File file : generatedFiles) {
526 getBuildContext().refresh(file);
527 }
528 }
529 }
530
531 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.ExceptionAsFlowControl" })
532 @NonNull
533 private List<File> performGeneration(
534 @NonNull IModuleLoader.IModulePostProcessor modulePostProcessor) throws MojoExecutionException {
535 File outputDir = getOutputDirectory();
536 if (getLog().isDebugEnabled()) {
537 getLog().debug(String.format("Using outputDirectory: %s", outputDir.getPath()));
538 }
539
540 if (!outputDir.exists() && !outputDir.mkdirs()) {
541 throw new MojoExecutionException("Unable to create output directory: " + outputDir);
542 }
543
544 IBindingContext bindingContext;
545 try {
546 bindingContext = newBindingContext(modulePostProcessor);
547 } catch (MetaschemaException | IOException ex) {
548 throw new MojoExecutionException("Failed to create the binding context", ex);
549 }
550
551
552 Set<IModule> modules;
553 try {
554 modules = getModulesToGenerateFor(bindingContext, modulePostProcessor);
555 } catch (Exception ex) {
556 throw new MojoExecutionException("Loading of metaschema modules failed", ex);
557 }
558
559 return generate(modules);
560 }
561
562
563
564
565
566
567
568
569
570
571
572 @NonNull
573 protected abstract List<File> generate(@NonNull Set<IModule> modules) throws MojoExecutionException;
574
575 protected final class LoggingValidationHandler
576 extends AbstractValidationResultProcessor {
577
578 private <T extends IValidationFinding> void handleFinding(
579 @NonNull T finding,
580 @NonNull Function<T, CharSequence> formatter) {
581
582 Log log = getLog();
583
584 switch (finding.getSeverity()) {
585 case CRITICAL:
586 case ERROR:
587 if (log.isErrorEnabled()) {
588 log.error(formatter.apply(finding), finding.getCause());
589 }
590 break;
591 case WARNING:
592 if (log.isWarnEnabled()) {
593 getLog().warn(formatter.apply(finding), finding.getCause());
594 }
595 break;
596 case INFORMATIONAL:
597 if (log.isInfoEnabled()) {
598 getLog().info(formatter.apply(finding), finding.getCause());
599 }
600 break;
601 default:
602 if (log.isDebugEnabled()) {
603 getLog().debug(formatter.apply(finding), finding.getCause());
604 }
605 break;
606 }
607 }
608
609 @Override
610 protected void handleJsonValidationFinding(JsonValidationFinding finding) {
611 handleFinding(finding, this::getMessage);
612 }
613
614 @Override
615 protected void handleXmlValidationFinding(XmlValidationFinding finding) {
616 handleFinding(finding, this::getMessage);
617 }
618
619 @Override
620 protected void handleConstraintValidationFinding(ConstraintValidationFinding finding) {
621 handleFinding(finding, this::getMessage);
622 }
623
624 @NonNull
625 private CharSequence getMessage(JsonValidationFinding finding) {
626 StringBuilder builder = new StringBuilder();
627 builder.append('[')
628 .append(finding.getCause().getPointerToViolation())
629 .append("] ")
630 .append(finding.getMessage());
631
632 URI documentUri = finding.getDocumentUri();
633 if (documentUri != null) {
634 builder.append(" [")
635 .append(documentUri.toString())
636 .append(']');
637 }
638 return builder;
639 }
640
641 @NonNull
642 private CharSequence getMessage(XmlValidationFinding finding) {
643 StringBuilder builder = new StringBuilder();
644
645 builder.append(finding.getMessage())
646 .append(" [");
647
648 URI documentUri = finding.getDocumentUri();
649 if (documentUri != null) {
650 builder.append(documentUri.toString());
651 }
652
653 SAXParseException ex = finding.getCause();
654 builder.append(finding.getMessage())
655 .append('{')
656 .append(ex.getLineNumber())
657 .append(',')
658 .append(ex.getColumnNumber())
659 .append("}]");
660 return builder;
661 }
662
663 @NonNull
664 private CharSequence getMessage(@NonNull ConstraintValidationFinding finding) {
665 StringBuilder builder = new StringBuilder();
666 builder.append('[')
667 .append(finding.getTarget().getMetapath())
668 .append(']');
669
670 String id = finding.getIdentifier();
671 if (id != null) {
672 builder.append(' ')
673 .append(id);
674 }
675
676 builder.append(' ')
677 .append(finding.getMessage());
678
679 URI documentUri = finding.getTarget().getBaseUri();
680 IResourceLocation location = finding.getLocation();
681 if (documentUri != null || location != null) {
682 builder.append(" [");
683 }
684
685 if (documentUri != null) {
686 builder.append(documentUri.toString());
687 }
688
689 if (location != null) {
690 builder.append('{')
691 .append(location.getLine())
692 .append(',')
693 .append(location.getColumn())
694 .append('}');
695 }
696 if (documentUri != null || location != null) {
697 builder.append(']');
698 }
699 return builder;
700 }
701 }
702
703 public class ModuleBindingGenerator implements IModuleBindingGenerator {
704 @NonNull
705 private final Path compilePath;
706 @NonNull
707 private final ClassLoader classLoader;
708 @NonNull
709 private final IBindingConfiguration bindingConfiguration;
710
711 public ModuleBindingGenerator(
712 @NonNull Path compilePath,
713 @NonNull IBindingConfiguration bindingConfiguration) {
714 this.compilePath = compilePath;
715 this.classLoader = ModuleCompilerHelper.newClassLoader(
716 compilePath,
717 ObjectUtils.notNull(Thread.currentThread().getContextClassLoader()));
718 this.bindingConfiguration = bindingConfiguration;
719 }
720
721 @NonNull
722 public IProduction generateClasses(@NonNull IModule module) throws MetaschemaException {
723 IProduction production;
724 try {
725 production = JavaGenerator.generate(module, compilePath, bindingConfiguration);
726 } catch (IOException ex) {
727 throw new MetaschemaException(
728 String.format("Unable to generate and compile classes for module '%s'.", module.getLocation()),
729 ex);
730 }
731 return production;
732 }
733
734 private void compileClasses(@NonNull IProduction production, @NonNull Path classDir)
735 throws IOException, DependencyResolutionRequiredException {
736 List<IGeneratedClass> classesToCompile = production.getGeneratedClasses().collect(Collectors.toList());
737
738 List<Path> classes = ObjectUtils.notNull(classesToCompile.stream()
739 .map(IGeneratedClass::getClassFile)
740 .collect(Collectors.toUnmodifiableList()));
741
742 JavaCompilerSupport compiler = new JavaCompilerSupport(classDir);
743 compiler.setLogger(new JavaCompilerSupport.Logger() {
744
745 @Override
746 public boolean isDebugEnabled() {
747 return getLog().isDebugEnabled();
748 }
749
750 @Override
751 public boolean isInfoEnabled() {
752 return getLog().isInfoEnabled();
753 }
754
755 @Override
756 public void debug(String msg) {
757 getLog().debug(msg);
758 }
759
760 @Override
761 public void info(String msg) {
762 getLog().info(msg);
763 }
764 });
765
766 getClassPath().forEach(compiler::addToClassPath);
767
768 JavaCompilerSupport.CompilationResult result = compiler.compile(classes);
769
770 if (!result.isSuccessful()) {
771 DiagnosticCollector<?> diagnostics = new DiagnosticCollector<>();
772 if (getLog().isErrorEnabled()) {
773 getLog().error("diagnostics: " + diagnostics.getDiagnostics().toString());
774 }
775 throw new IllegalStateException(String.format("failed to compile classes: %s",
776 classesToCompile.stream()
777 .map(clazz -> clazz.getClassName().canonicalName())
778 .collect(Collectors.joining(","))));
779 }
780 }
781
782 @Override
783 public Class<? extends IBoundModule> generate(IModule module) throws MetaschemaException {
784 IProduction production = generateClasses(module);
785 try {
786 compileClasses(production, compilePath);
787 } catch (IOException | DependencyResolutionRequiredException ex) {
788 throw new IllegalStateException("failed to compile classes", ex);
789 }
790 IGeneratedModuleClass moduleClass = ObjectUtils.requireNonNull(production.getModuleProduction(module));
791
792 try {
793 return moduleClass.load(classLoader);
794 } catch (ClassNotFoundException ex) {
795 throw new IllegalStateException(ex);
796 }
797 }
798 }
799
800 private static class LimitedExternalConstraintsModulePostProcessor
801 extends ExternalConstraintsModulePostProcessor {
802
803 public LimitedExternalConstraintsModulePostProcessor(
804 @NonNull Collection<IConstraintSet> additionalConstraintSets) {
805 super(additionalConstraintSets);
806 }
807
808
809
810
811
812
813 @Override
814 public void processModule(IModule module) {
815 if (!(module instanceof MetaschemaModelModule)) {
816 super.processModule(module);
817 }
818 }
819 }
820 }