1
2
3
4
5
6 package dev.metaschema.modules.sarif;
7
8 import org.schemastore.json.sarif.x210.Artifact;
9 import org.schemastore.json.sarif.x210.ArtifactLocation;
10 import org.schemastore.json.sarif.x210.Invocation;
11 import org.schemastore.json.sarif.x210.LetTimingEntry;
12 import org.schemastore.json.sarif.x210.Location;
13 import org.schemastore.json.sarif.x210.LogicalLocation;
14 import org.schemastore.json.sarif.x210.Message;
15 import org.schemastore.json.sarif.x210.MultiformatMessageString;
16 import org.schemastore.json.sarif.x210.Notification;
17 import org.schemastore.json.sarif.x210.PhysicalLocation;
18 import org.schemastore.json.sarif.x210.PropertyBag;
19 import org.schemastore.json.sarif.x210.Region;
20 import org.schemastore.json.sarif.x210.ReportingDescriptor;
21 import org.schemastore.json.sarif.x210.Result;
22 import org.schemastore.json.sarif.x210.Run;
23 import org.schemastore.json.sarif.x210.Sarif;
24 import org.schemastore.json.sarif.x210.SarifModule;
25 import org.schemastore.json.sarif.x210.TimingData;
26 import org.schemastore.json.sarif.x210.Tool;
27 import org.schemastore.json.sarif.x210.ToolComponent;
28
29 import java.io.IOException;
30 import java.io.StringWriter;
31 import java.math.BigDecimal;
32 import java.math.BigInteger;
33 import java.math.RoundingMode;
34 import java.net.URI;
35 import java.net.URISyntaxException;
36 import java.nio.file.Path;
37 import java.nio.file.StandardOpenOption;
38 import java.time.Instant;
39 import java.time.ZoneOffset;
40 import java.time.ZonedDateTime;
41 import java.util.ArrayList;
42 import java.util.Collection;
43 import java.util.LinkedHashMap;
44 import java.util.LinkedList;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Set;
48 import java.util.UUID;
49 import java.util.concurrent.ConcurrentHashMap;
50 import java.util.concurrent.atomic.AtomicInteger;
51
52 import dev.metaschema.core.datatype.markup.MarkupLine;
53 import dev.metaschema.core.datatype.markup.MarkupMultiline;
54 import dev.metaschema.core.metapath.item.node.INodeItem;
55 import dev.metaschema.core.model.IAttributable;
56 import dev.metaschema.core.model.IResourceLocation;
57 import dev.metaschema.core.model.MetaschemaException;
58 import dev.metaschema.core.model.constraint.ConstraintValidationFinding;
59 import dev.metaschema.core.model.constraint.IConstraint;
60 import dev.metaschema.core.model.constraint.IConstraint.Level;
61 import dev.metaschema.core.model.constraint.ILet;
62 import dev.metaschema.core.model.constraint.TimingCollector;
63 import dev.metaschema.core.model.constraint.TimingRecord;
64 import dev.metaschema.core.model.constraint.ValidationEventListener;
65 import dev.metaschema.core.model.constraint.ValidationPhase;
66 import dev.metaschema.core.model.validation.IValidationFinding;
67 import dev.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
68 import dev.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
69 import dev.metaschema.core.util.CollectionUtil;
70 import dev.metaschema.core.util.IVersionInfo;
71 import dev.metaschema.core.util.ObjectUtils;
72 import dev.metaschema.core.util.UriUtils;
73 import dev.metaschema.databind.IBindingContext;
74 import dev.metaschema.databind.io.Format;
75 import dev.metaschema.databind.io.SerializationFeature;
76 import edu.umd.cs.findbugs.annotations.NonNull;
77 import edu.umd.cs.findbugs.annotations.Nullable;
78
79
80
81
82
83 @SuppressWarnings("PMD.CouplingBetweenObjects")
84 public final class SarifValidationHandler implements ValidationEventListener {
85 private enum Kind {
86 NOT_APPLICABLE("notApplicable"),
87 PASS("pass"),
88 FAIL("fail"),
89 REVIEW("review"),
90 OPEN("open"),
91 INFORMATIONAL("informational");
92
93 @NonNull
94 private final String label;
95
96 Kind(@NonNull String label) {
97 this.label = label;
98 }
99
100 @NonNull
101 public String getLabel() {
102 return label;
103 }
104 }
105
106 private enum SeverityLevel {
107 NONE("none"),
108 NOTE("note"),
109 WARNING("warning"),
110 ERROR("error");
111
112 @NonNull
113 private final String label;
114
115 SeverityLevel(@NonNull String label) {
116 this.label = label;
117 }
118
119 @NonNull
120 public String getLabel() {
121 return label;
122 }
123 }
124
125 @NonNull
126 static final String SARIF_NS = "https://docs.oasis-open.org/sarif/sarif/v2.1.0";
127
128
129
130
131 @NonNull
132 public static final IAttributable.Key SARIF_HELP_URL_KEY
133 = IAttributable.key("help-url", SARIF_NS);
134
135
136
137 @NonNull
138 public static final IAttributable.Key SARIF_HELP_TEXT_KEY
139 = IAttributable.key("help-text", SARIF_NS);
140
141
142
143
144 @NonNull
145 public static final IAttributable.Key SARIF_HELP_MARKDOWN_KEY
146 = IAttributable.key("help-markdown", SARIF_NS);
147
148 @NonNull
149 private final URI source;
150 @Nullable
151 private final IVersionInfo toolVersion;
152 private final AtomicInteger artifactIndex = new AtomicInteger(-1);
153 private final AtomicInteger ruleIndex = new AtomicInteger(-1);
154
155 @SuppressWarnings("PMD.UseConcurrentHashMap")
156 @NonNull
157 private final Map<URI, ArtifactRecord> artifacts = new LinkedHashMap<>();
158 @NonNull
159 private final List<AbstractRuleRecord> rules = new LinkedList<>();
160 @SuppressWarnings("PMD.UseConcurrentHashMap")
161 @NonNull
162 private final Map<IConstraint, ConstraintRuleRecord> constraintRules = new LinkedHashMap<>();
163 @NonNull
164 private final List<IResult> results = new LinkedList<>();
165 @NonNull
166 private final SchemaRuleRecord schemaRule = new SchemaRuleRecord();
167 private boolean schemaValid = true;
168 @Nullable
169 private TimingCollector timingCollector;
170 @NonNull
171 private final Instant constructionTimestamp = Instant.now();
172 private final ThreadLocal<Long> currentEvaluationStartNanos = new ThreadLocal<>();
173 private final ThreadLocal<List<ConstraintResult>> currentEvaluationResults = new ThreadLocal<>();
174 private final ThreadLocal<Long> currentLetStartNanos = new ThreadLocal<>();
175 @SuppressWarnings("PMD.UseConcurrentHashMap")
176 private final ThreadLocal<Map<ILet, Long>> currentLetDurations = new ThreadLocal<>();
177 @NonNull
178 private final ConcurrentHashMap<IConstraint, EvaluationTimingSnapshot> evaluationTimings
179 = new ConcurrentHashMap<>();
180
181
182
183
184
185
186
187
188
189
190 public SarifValidationHandler(
191 @NonNull URI source,
192 @Nullable IVersionInfo toolVersion) {
193 if (!source.isAbsolute()) {
194 throw new IllegalArgumentException(String.format("The source URI '%s' is not absolute.", source.toASCIIString()));
195 }
196
197 this.source = source;
198 this.toolVersion = toolVersion;
199 }
200
201 @NonNull
202 private URI getSource() {
203 return source;
204 }
205
206 private IVersionInfo getToolVersion() {
207 return toolVersion;
208 }
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224 public void setTimingCollector(@Nullable TimingCollector collector) {
225 this.timingCollector = collector;
226 }
227
228 @Override
229 public void beforeValidation(@NonNull URI document) {
230
231 }
232
233 @Override
234 public void afterValidation(@NonNull URI document) {
235
236 }
237
238 @Override
239 public void beforePhase(@NonNull ValidationPhase phase) {
240
241 }
242
243 @Override
244 public void afterPhase(@NonNull ValidationPhase phase) {
245
246 }
247
248 @Override
249 public void beforeConstraintEvaluation(@NonNull IConstraint constraint, @NonNull INodeItem target) {
250 currentEvaluationStartNanos.set(System.nanoTime());
251 currentEvaluationResults.set(new ArrayList<>());
252 currentLetDurations.set(new LinkedHashMap<>());
253 }
254
255 @SuppressWarnings("PMD.NullAssignment")
256 @Override
257 public void afterConstraintEvaluation(@NonNull IConstraint constraint, @NonNull INodeItem target) {
258 Long startNanos = currentEvaluationStartNanos.get();
259 List<ConstraintResult> evaluationResults = currentEvaluationResults.get();
260 Map<ILet, Long> letDurations = currentLetDurations.get();
261
262 if (startNanos != null) {
263 long durationNs = System.nanoTime() - startNanos;
264 Map<ILet, Long> snapshotLetDurations = letDurations != null && !letDurations.isEmpty()
265 ? new LinkedHashMap<>(letDurations)
266 : null;
267
268
269 if (evaluationResults != null) {
270 for (ConstraintResult result : evaluationResults) {
271 result.setEvaluationDurationNs(durationNs);
272 if (snapshotLetDurations != null) {
273 result.setLetDurations(snapshotLetDurations);
274 }
275 }
276 }
277
278
279 evaluationTimings.put(constraint,
280 new EvaluationTimingSnapshot(durationNs, snapshotLetDurations));
281 }
282
283 currentEvaluationStartNanos.remove();
284 currentEvaluationResults.remove();
285 currentLetStartNanos.remove();
286 currentLetDurations.remove();
287 }
288
289 @Override
290 public void beforeLetEvaluation(@NonNull ILet let) {
291 currentLetStartNanos.set(System.nanoTime());
292 }
293
294 @Override
295 public void afterLetEvaluation(@NonNull ILet let) {
296 Long startNanos = currentLetStartNanos.get();
297 Map<ILet, Long> letDurations = currentLetDurations.get();
298 if (startNanos != null && letDurations != null) {
299 long durationNs = System.nanoTime() - startNanos;
300 letDurations.merge(let, durationNs, Long::sum);
301 }
302 currentLetStartNanos.remove();
303 }
304
305 @NonNull
306 private static final BigDecimal NS_PER_MS = BigDecimal.valueOf(1_000_000L);
307
308
309
310
311
312
313
314
315 @NonNull
316 private static BigDecimal nsToMs(long nanoseconds) {
317 return ObjectUtils.notNull(
318 BigDecimal.valueOf(nanoseconds).divide(NS_PER_MS, 3, RoundingMode.HALF_UP));
319 }
320
321
322
323
324
325
326
327
328 @NonNull
329 private static TimingData toTimingData(@NonNull TimingRecord record) {
330 TimingData data = new TimingData();
331 data.setTotalMs(nsToMs(record.getTotalTimeNs()));
332 data.setCount(BigInteger.valueOf(record.getCount()));
333 if (record.getCount() > 0) {
334 data.setMinMs(nsToMs(record.getMinTimeNs()));
335 data.setMaxMs(nsToMs(record.getMaxTimeNs()));
336 }
337 return data;
338 }
339
340
341
342
343
344
345
346 public void addFindings(@NonNull Collection<? extends IValidationFinding> findings) {
347 for (IValidationFinding finding : findings) {
348 assert finding != null;
349 addFinding(finding);
350 }
351 }
352
353
354
355
356
357
358
359 public void addFinding(@NonNull IValidationFinding finding) {
360 if (finding instanceof JsonValidationFinding) {
361 addJsonValidationFinding((JsonValidationFinding) finding);
362 } else if (finding instanceof XmlValidationFinding) {
363 addXmlValidationFinding((XmlValidationFinding) finding);
364 } else if (finding instanceof ConstraintValidationFinding) {
365 addConstraintValidationFinding((ConstraintValidationFinding) finding);
366 } else {
367 throw new IllegalStateException();
368 }
369 }
370
371 private ConstraintRuleRecord getRuleRecord(@NonNull IConstraint constraint) {
372 ConstraintRuleRecord retval = constraintRules.get(constraint);
373 if (retval == null) {
374 retval = new ConstraintRuleRecord(constraint);
375 constraintRules.put(constraint, retval);
376 rules.add(retval);
377 }
378 return retval;
379 }
380
381 private ArtifactRecord getArtifactRecord(@NonNull URI artifactUri) {
382 ArtifactRecord retval = artifacts.get(artifactUri);
383 if (retval == null) {
384 retval = new ArtifactRecord(artifactUri);
385 artifacts.put(artifactUri, retval);
386 }
387 return retval;
388 }
389
390 private void addJsonValidationFinding(@NonNull JsonValidationFinding finding) {
391 results.add(new SchemaResult(finding));
392 if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) {
393 schemaValid = false;
394 }
395 }
396
397 private void addXmlValidationFinding(@NonNull XmlValidationFinding finding) {
398 results.add(new SchemaResult(finding));
399 if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) {
400 schemaValid = false;
401 }
402 }
403
404 private void addConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) {
405 ConstraintResult constraintResult = new ConstraintResult(finding);
406 results.add(constraintResult);
407
408
409 List<ConstraintResult> evaluationResults = currentEvaluationResults.get();
410 if (evaluationResults != null) {
411 evaluationResults.add(constraintResult);
412 } else {
413
414 for (IConstraint constraint : finding.getConstraints()) {
415 EvaluationTimingSnapshot snapshot = evaluationTimings.get(constraint);
416 if (snapshot != null) {
417 constraintResult.setEvaluationDurationNs(snapshot.durationNs);
418 if (snapshot.letDurations != null) {
419 constraintResult.setLetDurations(snapshot.letDurations);
420 }
421 break;
422 }
423 }
424 }
425 }
426
427
428
429
430
431
432
433
434
435
436 @NonNull
437 private Sarif generateSarif(@NonNull URI outputUri) throws IOException {
438 Sarif sarif = new Sarif();
439 sarif.setVersion("2.1.0");
440
441 Run run = new Run();
442 sarif.addRun(run);
443
444 Artifact artifact = new Artifact();
445 artifact.setLocation(getArtifactRecord(getSource()).generateArtifactLocation(outputUri));
446 run.addArtifact(artifact);
447
448 for (IResult result : results) {
449 result.generateResults(outputUri).forEach(run::addResult);
450 }
451
452 IVersionInfo toolVersion = getToolVersion();
453 if (!rules.isEmpty() || toolVersion != null) {
454 Tool tool = new Tool();
455 ToolComponent driver = new ToolComponent();
456
457 if (toolVersion != null) {
458 driver.setName(toolVersion.getName());
459 driver.setVersion(toolVersion.getVersion());
460 }
461
462 for (AbstractRuleRecord rule : rules) {
463 driver.addRule(rule.generate());
464 }
465
466 tool.setDriver(driver);
467 run.setTool(tool);
468 }
469
470 enrichWithTiming(run);
471
472 return sarif;
473 }
474
475
476
477
478
479
480
481
482
483
484
485 @SuppressWarnings("PMD.CognitiveComplexity")
486 private void enrichWithTiming(@NonNull Run run) {
487
488 Invocation invocation = new Invocation();
489 invocation.setExecutionSuccessful(Boolean.TRUE);
490 invocation.setStartTimeUtc(ZonedDateTime.ofInstant(constructionTimestamp, ZoneOffset.UTC));
491 invocation.setEndTimeUtc(ZonedDateTime.ofInstant(Instant.now(), ZoneOffset.UTC));
492
493 TimingCollector collector = this.timingCollector;
494 if (collector != null) {
495
496 TimingRecord validationTiming = collector.getValidationTiming();
497 if (validationTiming != null) {
498 Instant start = validationTiming.getStartTimestampUtc();
499 if (start != null) {
500 invocation.setStartTimeUtc(ZonedDateTime.ofInstant(start, ZoneOffset.UTC));
501 }
502 Instant end = validationTiming.getEndTimestampUtc();
503 if (end != null) {
504 invocation.setEndTimeUtc(ZonedDateTime.ofInstant(end, ZoneOffset.UTC));
505 }
506 }
507
508
509 for (Map.Entry<ValidationPhase, TimingRecord> entry : collector.getPhaseTimings().entrySet()) {
510 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
511 Notification notification = new Notification();
512 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
513 Message msg = new Message();
514 msg.setText("Phase: " + entry.getKey().name());
515 notification.setMessage(msg);
516
517 TimingRecord phaseRecord = entry.getValue();
518 Instant phaseEnd = phaseRecord.getEndTimestampUtc();
519 if (phaseEnd != null) {
520 notification.setTimeUtc(ZonedDateTime.ofInstant(phaseEnd, ZoneOffset.UTC));
521 }
522
523 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
524 PropertyBag phaseProps = new PropertyBag();
525 phaseProps.setTiming(toTimingData(phaseRecord));
526 notification.setProperties(phaseProps);
527
528 invocation.addToolExecutionNotification(notification);
529 }
530
531
532 for (Map.Entry<ILet, TimingRecord> entry : collector.getLetTimings().entrySet()) {
533 ILet let = entry.getKey();
534
535 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
536 Notification notification = new Notification();
537 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
538 Message msg = new Message();
539 msg.setText("$" + let.getName().getLocalName() + " := " + let.getValueExpression().getPath());
540 notification.setMessage(msg);
541
542 TimingRecord letRecord = entry.getValue();
543 Instant letEnd = letRecord.getEndTimestampUtc();
544 if (letEnd != null) {
545 notification.setTimeUtc(ZonedDateTime.ofInstant(letEnd, ZoneOffset.UTC));
546 }
547
548 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
549 PropertyBag letProps = new PropertyBag();
550 letProps.setTiming(toTimingData(letRecord));
551 notification.setProperties(letProps);
552
553 invocation.addToolExecutionNotification(notification);
554 }
555 }
556
557 run.addInvocation(invocation);
558 }
559
560
561
562
563
564
565
566
567
568
569
570 @NonNull
571 public String writeToString(@NonNull IBindingContext bindingContext) throws IOException {
572 registerSarifMetaschemaModule(bindingContext);
573 try (StringWriter writer = new StringWriter()) {
574 bindingContext.newSerializer(Format.JSON, Sarif.class)
575 .disableFeature(SerializationFeature.SERIALIZE_ROOT)
576 .serialize(generateSarif(getSource()), writer);
577 return ObjectUtils.notNull(writer.toString());
578 }
579 }
580
581
582
583
584
585
586
587
588
589
590
591
592 public void write(
593 @NonNull Path outputFile,
594 @NonNull IBindingContext bindingContext) throws IOException {
595
596 URI output = ObjectUtils.notNull(outputFile.toUri());
597 Sarif sarif = generateSarif(output);
598
599 registerSarifMetaschemaModule(bindingContext);
600 bindingContext.newSerializer(Format.JSON, Sarif.class)
601 .disableFeature(SerializationFeature.SERIALIZE_ROOT)
602 .serialize(
603 sarif,
604 outputFile,
605 StandardOpenOption.CREATE,
606 StandardOpenOption.WRITE,
607 StandardOpenOption.TRUNCATE_EXISTING);
608 }
609
610 private static void registerSarifMetaschemaModule(@NonNull IBindingContext bindingContext) {
611 try {
612 bindingContext.registerModule(SarifModule.class);
613 } catch (MetaschemaException ex) {
614 throw new IllegalStateException("Unable to register the builtin SARIF module.", ex);
615 }
616 }
617
618 private interface IResult {
619 @NonNull
620 IValidationFinding getFinding();
621
622 @NonNull
623 List<Result> generateResults(@NonNull URI output) throws IOException;
624 }
625
626 private abstract class AbstractResult<T extends IValidationFinding> implements IResult {
627 @NonNull
628 private final T finding;
629
630 protected AbstractResult(@NonNull T finding) {
631 this.finding = finding;
632 }
633
634 @Override
635 public T getFinding() {
636 return finding;
637 }
638
639 @NonNull
640 protected Kind kind(@NonNull IValidationFinding finding) {
641 IValidationFinding.Kind kind = finding.getKind();
642
643 Kind retval;
644 switch (kind) {
645 case FAIL:
646 retval = Kind.FAIL;
647 break;
648 case INFORMATIONAL:
649 retval = Kind.INFORMATIONAL;
650 break;
651 case NOT_APPLICABLE:
652 retval = Kind.NOT_APPLICABLE;
653 break;
654 case PASS:
655 retval = Kind.PASS;
656 break;
657 default:
658 throw new IllegalArgumentException(String.format("Invalid finding kind '%s'.", kind));
659 }
660 return retval;
661 }
662
663 @NonNull
664 protected SeverityLevel level(@NonNull Level severity) {
665 SeverityLevel retval;
666 switch (severity) {
667 case CRITICAL:
668 case ERROR:
669 retval = SeverityLevel.ERROR;
670 break;
671 case INFORMATIONAL:
672 case DEBUG:
673 retval = SeverityLevel.NOTE;
674 break;
675 case WARNING:
676 retval = SeverityLevel.WARNING;
677 break;
678 case NONE:
679 retval = SeverityLevel.NONE;
680 break;
681 default:
682 throw new IllegalArgumentException(String.format("Invalid severity '%s'.", severity));
683 }
684 return retval;
685 }
686
687 protected void message(@NonNull IValidationFinding finding, @NonNull Result result) {
688 String message = finding.getMessage();
689 if (message == null) {
690 message = "";
691 }
692
693 Message msg = new Message();
694 msg.setText(message);
695 result.setMessage(msg);
696 }
697
698 protected void location(@NonNull IValidationFinding finding, @NonNull Result result, @NonNull URI base)
699 throws IOException {
700 IResourceLocation location = finding.getLocation();
701 if (location != null) {
702
703 Region region = new Region();
704
705 if (location.getLine() > -1) {
706 region.setStartLine(BigInteger.valueOf(location.getLine()));
707 region.setEndLine(BigInteger.valueOf(location.getLine()));
708 }
709 if (location.getColumn() > -1) {
710 region.setStartColumn(BigInteger.valueOf(location.getColumn() + 1));
711 region.setEndColumn(BigInteger.valueOf(location.getColumn() + 1));
712 }
713 if (location.getByteOffset() > -1) {
714 region.setByteOffset(BigInteger.valueOf(location.getByteOffset()));
715 region.setByteLength(BigInteger.ZERO);
716 }
717 if (location.getCharOffset() > -1) {
718 region.setCharOffset(BigInteger.valueOf(location.getCharOffset()));
719 region.setCharLength(BigInteger.ZERO);
720 }
721
722 PhysicalLocation physical = new PhysicalLocation();
723
724 URI documentUri = finding.getDocumentUri();
725 if (documentUri != null) {
726 physical.setArtifactLocation(getArtifactRecord(documentUri).generateArtifactLocation(base));
727 }
728 physical.setRegion(region);
729
730 LogicalLocation logical = new LogicalLocation();
731
732 logical.setDecoratedName(finding.getPath());
733
734 Location loc = new Location();
735 loc.setPhysicalLocation(physical);
736 loc.addLogicalLocation(logical);
737 result.addLocation(loc);
738 }
739 }
740 }
741
742 private final class SchemaResult
743 extends AbstractResult<IValidationFinding> {
744
745 protected SchemaResult(@NonNull IValidationFinding finding) {
746 super(finding);
747 }
748
749 @Override
750 public List<Result> generateResults(@NonNull URI output) throws IOException {
751 IValidationFinding finding = getFinding();
752
753 Result result = new Result();
754
755 result.setRuleId(schemaRule.getId());
756 result.setRuleIndex(BigInteger.valueOf(schemaRule.getIndex()));
757 result.setGuid(schemaRule.getGuid());
758
759 result.setKind(kind(finding).getLabel());
760 result.setLevel(level(finding.getSeverity()).getLabel());
761 message(finding, result);
762 location(finding, result, output);
763
764 return CollectionUtil.singletonList(result);
765 }
766 }
767
768 private final class ConstraintResult
769 extends AbstractResult<ConstraintValidationFinding> {
770 @Nullable
771 private Long evaluationDurationNs;
772 @Nullable
773 private Map<ILet, Long> letDurations;
774
775 protected ConstraintResult(@NonNull ConstraintValidationFinding finding) {
776 super(finding);
777 }
778
779 void setEvaluationDurationNs(long durationNs) {
780 this.evaluationDurationNs = durationNs;
781 }
782
783 void setLetDurations(@NonNull Map<ILet, Long> durations) {
784 this.letDurations = durations;
785 }
786
787 @Override
788 public List<Result> generateResults(@NonNull URI output) throws IOException {
789 ConstraintValidationFinding finding = getFinding();
790
791 List<Result> retval = new LinkedList<>();
792
793 Kind kind = kind(finding);
794 SeverityLevel level = level(finding.getSeverity());
795
796 for (IConstraint constraint : finding.getConstraints()) {
797 assert constraint != null;
798 ConstraintRuleRecord rule = getRuleRecord(constraint);
799
800 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
801 Result result = new Result();
802
803 String id = constraint.getId();
804 if (id != null) {
805 result.setRuleId(id);
806 }
807 result.setRuleIndex(BigInteger.valueOf(rule.getIndex()));
808 result.setGuid(rule.getGuid());
809 result.setKind(kind.getLabel());
810 result.setLevel(level.getLabel());
811 message(finding, result);
812 location(finding, result, output);
813 addPerResultTiming(result);
814
815 retval.add(result);
816 }
817 return retval;
818 }
819
820 @SuppressWarnings("PMD.CognitiveComplexity")
821 private void addPerResultTiming(@NonNull Result result) {
822 Long durationNs = this.evaluationDurationNs;
823 if (durationNs == null) {
824 return;
825 }
826
827 PropertyBag props = result.getProperties();
828 if (props == null) {
829 props = new PropertyBag();
830 result.setProperties(props);
831 }
832
833 TimingData timing = new TimingData();
834 timing.setTotalMs(nsToMs(durationNs));
835 timing.setCount(BigInteger.ONE);
836 props.setTiming(timing);
837
838 Map<ILet, Long> letDurs = this.letDurations;
839 if (letDurs != null) {
840 for (Map.Entry<ILet, Long> entry : letDurs.entrySet()) {
841 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
842 LetTimingEntry letEntry = new LetTimingEntry();
843 letEntry.setName(entry.getKey().getName().getLocalName());
844
845 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
846 TimingData letTiming = new TimingData();
847 letTiming.setTotalMs(nsToMs(entry.getValue()));
848 letTiming.setCount(BigInteger.ONE);
849 letEntry.setTiming(letTiming);
850
851 props.addLetTimingEntry(letEntry);
852 }
853 }
854 }
855 }
856
857 private abstract class AbstractRuleRecord {
858 private final int index;
859 @NonNull
860 private final UUID guid;
861
862 private AbstractRuleRecord() {
863 this.index = ruleIndex.addAndGet(1);
864 this.guid = ObjectUtils.notNull(UUID.randomUUID());
865 }
866
867 public int getIndex() {
868 return index;
869 }
870
871 @NonNull
872 public UUID getGuid() {
873 return guid;
874 }
875
876 @NonNull
877 protected abstract ReportingDescriptor generate();
878 }
879
880 private final class SchemaRuleRecord
881 extends AbstractRuleRecord {
882
883 @Override
884 protected ReportingDescriptor generate() {
885 ReportingDescriptor retval = new ReportingDescriptor();
886 retval.setId(getId());
887 retval.setGuid(getGuid());
888 return retval;
889 }
890
891 public String getId() {
892 return "schema-valid";
893 }
894 }
895
896 private final class ConstraintRuleRecord
897 extends AbstractRuleRecord {
898 @NonNull
899 private final IConstraint constraint;
900
901 public ConstraintRuleRecord(@NonNull IConstraint constraint) {
902 this.constraint = constraint;
903 }
904
905 @NonNull
906 public IConstraint getConstraint() {
907 return constraint;
908 }
909
910 @Override
911 protected ReportingDescriptor generate() {
912 ReportingDescriptor retval = new ReportingDescriptor();
913 IConstraint constraint = getConstraint();
914
915 UUID guid = getGuid();
916
917 String id = constraint.getId();
918 if (id == null) {
919 retval.setId(guid.toString());
920 } else {
921 retval.setId(id);
922 }
923 retval.setGuid(guid);
924 String formalName = constraint.getFormalName();
925 if (formalName != null) {
926 MultiformatMessageString text = new MultiformatMessageString();
927 text.setText(formalName);
928 retval.setShortDescription(text);
929 }
930 MarkupLine description = constraint.getDescription();
931 if (description != null) {
932 MultiformatMessageString text = new MultiformatMessageString();
933 text.setText(description.toText());
934 text.setMarkdown(description.toMarkdown());
935 retval.setFullDescription(text);
936 }
937
938 Set<String> helpUrls = constraint.getPropertyValues(SARIF_HELP_URL_KEY);
939 if (!helpUrls.isEmpty()) {
940 retval.setHelpUri(URI.create(helpUrls.stream().findFirst().get()));
941 }
942
943 Set<String> helpText = constraint.getPropertyValues(SARIF_HELP_TEXT_KEY);
944 Set<String> helpMarkdown = constraint.getPropertyValues(SARIF_HELP_MARKDOWN_KEY);
945
946 if (!helpText.isEmpty() || !helpMarkdown.isEmpty()) {
947 MultiformatMessageString help = new MultiformatMessageString();
948
949 MarkupMultiline markdown = helpMarkdown.stream().map(MarkupMultiline::fromMarkdown).findFirst().orElse(null);
950 if (markdown != null) {
951
952 help.setMarkdown(markdown.toMarkdown());
953 }
954
955 String text = helpText.isEmpty()
956 ? ObjectUtils.requireNonNull(markdown).toText()
957 : helpText.stream().findFirst().get();
958 help.setText(text);
959
960 retval.setHelp(help);
961 }
962
963
964 TimingCollector collector = timingCollector;
965 if (collector != null) {
966 TimingRecord record = collector.getConstraintTiming(constraint.getInternalIdentifier());
967 if (record != null) {
968 PropertyBag props = retval.getProperties();
969 if (props == null) {
970 props = new PropertyBag();
971 retval.setProperties(props);
972 }
973 props.setTiming(toTimingData(record));
974 }
975 }
976
977 return retval;
978 }
979
980 }
981
982 private final class ArtifactRecord {
983 @NonNull
984 private final URI uri;
985 private final int index;
986
987 public ArtifactRecord(@NonNull URI uri) {
988 this.uri = uri;
989 this.index = artifactIndex.addAndGet(1);
990 }
991
992 @NonNull
993 public URI getUri() {
994 return uri;
995 }
996
997 public int getIndex() {
998 return index;
999 }
1000
1001 public ArtifactLocation generateArtifactLocation(@NonNull URI baseUri) throws IOException {
1002 ArtifactLocation location = new ArtifactLocation();
1003
1004 try {
1005 location.setUri(UriUtils.relativize(baseUri, getUri(), true));
1006 } catch (URISyntaxException ex) {
1007 throw new IOException(ex);
1008 }
1009
1010 location.setIndex(BigInteger.valueOf(getIndex()));
1011 return location;
1012 }
1013 }
1014
1015
1016
1017
1018
1019 private static final class EvaluationTimingSnapshot {
1020 final long durationNs;
1021 @Nullable
1022 final Map<ILet, Long> letDurations;
1023
1024 EvaluationTimingSnapshot(long durationNs, @Nullable Map<ILet, Long> letDurations) {
1025 this.durationNs = durationNs;
1026 this.letDurations = letDurations;
1027 }
1028 }
1029 }