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