1
2
3
4
5
6 package dev.metaschema.cli.util;
7
8 import static org.jline.jansi.Ansi.ansi;
9
10 import org.apache.logging.log4j.LogBuilder;
11 import org.apache.logging.log4j.LogManager;
12 import org.apache.logging.log4j.Logger;
13 import org.jline.jansi.Ansi;
14 import org.jline.jansi.Ansi.Color;
15 import org.xml.sax.SAXParseException;
16
17 import java.net.URI;
18 import java.util.Set;
19 import java.util.stream.Collectors;
20
21 import dev.metaschema.core.metapath.format.IPathFormatter;
22 import dev.metaschema.core.model.constraint.ConstraintValidationFinding;
23 import dev.metaschema.core.model.constraint.IConstraint.Level;
24 import dev.metaschema.core.model.validation.AbstractValidationResultProcessor;
25 import dev.metaschema.core.model.validation.IValidationFinding;
26 import dev.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
27 import dev.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
28 import dev.metaschema.modules.sarif.SarifValidationHandler;
29 import edu.umd.cs.findbugs.annotations.NonNull;
30 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
31
32
33
34
35
36 public final class LoggingValidationHandler
37 extends AbstractValidationResultProcessor {
38 private static final Logger LOGGER = LogManager.getLogger(LoggingValidationHandler.class);
39
40 @NonNull
41 private static final LoggingValidationHandler NO_LOG_EXCPTION_INSTANCE = new LoggingValidationHandler(false);
42 @NonNull
43 private static final LoggingValidationHandler LOG_EXCPTION_INSTANCE = new LoggingValidationHandler(true);
44
45 private final boolean logExceptions;
46 @NonNull
47 private final IPathFormatter pathFormatter;
48
49
50
51
52
53
54
55
56 @NonNull
57 public static LoggingValidationHandler instance() {
58 return instance(false);
59 }
60
61
62
63
64
65
66
67
68
69 @SuppressFBWarnings(value = "SING_SINGLETON_GETTER_NOT_SYNCHRONIZED",
70 justification = "both values are class initialized")
71 @NonNull
72 public static LoggingValidationHandler instance(boolean logExceptions) {
73 return logExceptions ? LOG_EXCPTION_INSTANCE : NO_LOG_EXCPTION_INSTANCE;
74 }
75
76
77
78
79
80
81
82
83 @NonNull
84 public static LoggingValidationHandler withPathFormatter(@NonNull IPathFormatter pathFormatter) {
85 return new LoggingValidationHandler(false, pathFormatter);
86 }
87
88
89
90
91
92
93
94
95
96
97
98 @NonNull
99 public static LoggingValidationHandler withSettings(
100 boolean logExceptions,
101 @NonNull IPathFormatter pathFormatter) {
102 return new LoggingValidationHandler(logExceptions, pathFormatter);
103 }
104
105 private LoggingValidationHandler(boolean logExceptions) {
106 this(logExceptions, IPathFormatter.METAPATH_PATH_FORMATER);
107 }
108
109 private LoggingValidationHandler(boolean logExceptions, @NonNull IPathFormatter pathFormatter) {
110 this.logExceptions = logExceptions;
111 this.pathFormatter = pathFormatter;
112 }
113
114
115
116
117
118
119 public boolean isLogExceptions() {
120 return logExceptions;
121 }
122
123 @Override
124 protected void handleJsonValidationFinding(@NonNull JsonValidationFinding finding) {
125 Ansi ansi = generatePreamble(finding.getSeverity());
126
127 ansi = ansi.a('[')
128 .fgBright(Color.WHITE)
129 .a(finding.getCause().getPointerToViolation())
130 .reset()
131 .a(']');
132
133 URI documentUri = finding.getDocumentUri();
134 ansi = documentUri == null
135 ? ansi.format(" %s", finding.getMessage())
136 : ansi.format(" %s [%s]", finding.getMessage(), documentUri.toString());
137
138 getLogger(finding).log(ansi);
139 }
140
141 @Override
142 protected void handleXmlValidationFinding(XmlValidationFinding finding) {
143 Ansi ansi = generatePreamble(finding.getSeverity());
144 SAXParseException ex = finding.getCause();
145
146 URI documentUri = finding.getDocumentUri();
147 ansi = documentUri == null
148 ? ansi.format("%s [{%d,%d}]",
149 finding.getMessage(),
150 ex.getLineNumber(),
151 ex.getColumnNumber())
152 : ansi.format("%s [%s{%d,%d}]",
153 finding.getMessage(),
154 documentUri.toString(),
155 ex.getLineNumber(),
156 ex.getColumnNumber());
157
158 getLogger(finding).log(ansi);
159 }
160
161 @Override
162 protected void handleConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) {
163 Ansi ansi = generatePreamble(finding.getSeverity());
164
165 ansi.format("[%s]", finding.getTarget().toPath(pathFormatter));
166
167 String id = finding.getIdentifier();
168 if (id != null) {
169 ansi.format(" %s:", id);
170 }
171
172 ansi.format(" %s", finding.getMessage());
173
174 Set<String> helpUrls = finding.getConstraints().stream()
175 .flatMap(constraint -> constraint.getPropertyValues(SarifValidationHandler.SARIF_HELP_URL_KEY).stream())
176 .collect(Collectors.toSet());
177 if (!helpUrls.isEmpty()) {
178 ansi.format(" (help: %s)",
179 helpUrls.stream().collect(Collectors.joining(", ")));
180 }
181
182 getLogger(finding).log(ansi);
183 }
184
185 @NonNull
186 private LogBuilder getLogger(@NonNull IValidationFinding finding) {
187 LogBuilder retval;
188 switch (finding.getSeverity()) {
189 case CRITICAL:
190 retval = LOGGER.atFatal();
191 break;
192 case ERROR:
193 retval = LOGGER.atError();
194 break;
195 case WARNING:
196 retval = LOGGER.atWarn();
197 break;
198 case INFORMATIONAL:
199 retval = LOGGER.atInfo();
200 break;
201 case DEBUG:
202 retval = LOGGER.isDebugEnabled() ? LOGGER.atDebug() : LOGGER.atInfo();
203 break;
204 default:
205 throw new IllegalArgumentException("Unknown level: " + finding.getSeverity().name());
206 }
207
208 assert retval != null;
209
210 if (finding.getCause() != null && isLogExceptions()) {
211 retval.withThrowable(finding.getCause());
212 }
213
214 return retval;
215 }
216
217 @SuppressWarnings("static-method")
218 @NonNull
219 private Ansi generatePreamble(@NonNull Level level) {
220 Ansi ansi = ansi().fgBright(Color.WHITE).a('[').reset();
221
222 switch (level) {
223 case CRITICAL:
224 ansi = ansi.fgRed().a("CRITICAL").reset();
225 break;
226 case ERROR:
227 ansi = ansi.fgBrightRed().a("ERROR").reset();
228 break;
229 case WARNING:
230 ansi = ansi.fgBrightYellow().a("WARNING").reset();
231 break;
232 case INFORMATIONAL:
233 ansi = ansi.fgBrightBlue().a("INFO").reset();
234 break;
235 case DEBUG:
236 ansi = ansi.fgBrightCyan().a("DEBUG").reset();
237 break;
238 default:
239 ansi = ansi().fgBright(Color.MAGENTA).a(level.name()).reset();
240 break;
241 }
242 ansi = ansi.fgBright(Color.WHITE).a("] ").reset();
243
244 assert ansi != null;
245 return ansi;
246 }
247 }