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