1
2
3
4
5
6 package dev.metaschema.core.model.validation;
7
8 import org.eclipse.jdt.annotation.Owning;
9 import org.xml.sax.ErrorHandler;
10 import org.xml.sax.SAXException;
11 import org.xml.sax.SAXParseException;
12
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.net.URI;
16 import java.util.Collections;
17 import java.util.LinkedList;
18 import java.util.List;
19
20 import javax.xml.XMLConstants;
21 import javax.xml.transform.Source;
22 import javax.xml.transform.stream.StreamSource;
23 import javax.xml.validation.Schema;
24 import javax.xml.validation.SchemaFactory;
25 import javax.xml.validation.Validator;
26
27 import dev.metaschema.core.model.IResourceLocation;
28 import dev.metaschema.core.model.constraint.IConstraint.Level;
29 import dev.metaschema.core.util.ObjectUtils;
30 import edu.umd.cs.findbugs.annotations.NonNull;
31
32
33
34
35 public class XmlSchemaContentValidator
36 extends AbstractContentValidator {
37 private final Schema schema;
38
39 @SuppressWarnings({ "resource", "PMD.UseTryWithResources" })
40 @NonNull
41 private static Schema toSchema(@NonNull List<? extends Source> schemaSources) throws IOException {
42 SchemaFactory schemafactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
43
44
45 try {
46 return ObjectUtils.notNull(schemaSources.isEmpty()
47 ? schemafactory.newSchema()
48 : schemafactory.newSchema(schemaSources.toArray(new Source[0])));
49 } catch (SAXException ex) {
50 throw new IOException(ex);
51 } finally {
52
53 for (Source source : schemaSources) {
54 if (source instanceof StreamSource) {
55 StreamSource streamSource = (StreamSource) source;
56 if (streamSource.getInputStream() != null) {
57 streamSource.getInputStream().close();
58 }
59 }
60 }
61 }
62 }
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 public XmlSchemaContentValidator(@Owning @NonNull List<? extends Source> schemaSources) throws IOException {
78 this(toSchema(ObjectUtils.requireNonNull(schemaSources, "schemaSources")));
79 }
80
81
82
83
84
85
86
87
88 protected XmlSchemaContentValidator(@NonNull Schema schema) {
89 this.schema = ObjectUtils.requireNonNull(schema, "schema");
90 }
91
92 private Schema getSchema() {
93 return schema;
94 }
95
96 @Override
97 public IValidationResult validate(InputStream is, URI documentUri) throws IOException {
98 Source xmlSource = new StreamSource(is, documentUri.toASCIIString());
99
100 Validator validator = getSchema().newValidator();
101 XmlValidationErrorHandler errorHandler = new XmlValidationErrorHandler(documentUri);
102 validator.setErrorHandler(errorHandler);
103 try {
104 validator.validate(xmlSource);
105 } catch (SAXParseException ex) {
106 String location = ex.getLineNumber() > -1 && ex.getColumnNumber() > -1
107 ? String.format("at %d:%d", ex.getLineNumber(), ex.getColumnNumber())
108 : "";
109 throw new IOException(
110 String.format("Unexpected failure during validation of '%s'%s. %s",
111 documentUri,
112 location,
113 ex.getLocalizedMessage()),
114 ex);
115 } catch (SAXException ex) {
116 throw new IOException(
117 String.format("Unexpected failure during validation of '%s'. %s",
118 documentUri,
119 ex.getLocalizedMessage()),
120 ex);
121 }
122 return errorHandler;
123 }
124
125
126
127
128
129 public static class XmlValidationFinding implements IValidationFinding, IResourceLocation {
130 @NonNull
131 private final URI documentUri;
132 @NonNull
133 private final SAXParseException exception;
134 @NonNull
135 private final Level severity;
136
137
138
139
140
141
142
143
144
145
146
147
148
149 public XmlValidationFinding(
150 @NonNull Level severity,
151 @NonNull SAXParseException exception,
152 @NonNull URI resourceUri) {
153 this.severity = ObjectUtils.requireNonNull(severity, "severity");
154 this.exception = ObjectUtils.requireNonNull(exception, "exception");
155 this.documentUri = ObjectUtils.requireNonNull(resourceUri, "documentUri");
156 }
157
158 @Override
159 public String getIdentifier() {
160
161 return null;
162 }
163
164 @Override
165 public Kind getKind() {
166 return getSeverity() == Level.WARNING ? Kind.PASS : Kind.FAIL;
167 }
168
169 @Override
170 public Level getSeverity() {
171 return severity;
172 }
173
174 @Override
175 public URI getDocumentUri() {
176 String systemId = getCause().getSystemId();
177 return systemId == null ? documentUri : URI.create(systemId);
178 }
179
180 @Override
181 public int getLine() {
182 return getCause().getLineNumber();
183 }
184
185 @Override
186 public int getColumn() {
187 return getCause().getColumnNumber();
188 }
189
190 @Override
191 public long getCharOffset() {
192
193 return -1;
194 }
195
196 @Override
197 public long getByteOffset() {
198
199 return -1;
200 }
201
202 @Override
203 public IResourceLocation getLocation() {
204 return this;
205 }
206
207 @Override
208 public String getPathKind() {
209
210 return null;
211 }
212
213 @Override
214 public String getPath() {
215
216 return null;
217 }
218
219 @Override
220 public String getMessage() {
221 return getCause().getLocalizedMessage();
222 }
223
224 @NonNull
225 @Override
226 public SAXParseException getCause() {
227 return exception;
228 }
229 }
230
231 private static class XmlValidationErrorHandler implements ErrorHandler, IValidationResult {
232 @NonNull
233 private final URI documentUri;
234 @NonNull
235 private final List<XmlValidationFinding> findings = new LinkedList<>();
236 @NonNull
237 private Level highestSeverity = Level.INFORMATIONAL;
238
239 public XmlValidationErrorHandler(@NonNull URI documentUri) {
240 this.documentUri = ObjectUtils.requireNonNull(documentUri, "documentUri");
241 }
242
243 @NonNull
244 public URI getDocumentUri() {
245 return documentUri;
246 }
247
248 private void adjustHighestSeverity(@NonNull Level severity) {
249 if (highestSeverity.ordinal() < severity.ordinal()) {
250 highestSeverity = severity;
251 }
252 }
253
254 @SuppressWarnings("null")
255 @Override
256 public void warning(SAXParseException ex) throws SAXException {
257 findings.add(new XmlValidationFinding(Level.WARNING, ex, getDocumentUri()));
258 adjustHighestSeverity(Level.WARNING);
259 }
260
261 @SuppressWarnings("null")
262 @Override
263 public void error(SAXParseException ex) throws SAXException {
264 findings.add(new XmlValidationFinding(Level.ERROR, ex, getDocumentUri()));
265 adjustHighestSeverity(Level.CRITICAL);
266 }
267
268 @SuppressWarnings("null")
269 @Override
270 public void fatalError(SAXParseException ex) throws SAXException {
271 findings.add(new XmlValidationFinding(Level.CRITICAL, ex, getDocumentUri()));
272 adjustHighestSeverity(Level.CRITICAL);
273 }
274
275 @SuppressWarnings("null")
276 @Override
277 @NonNull
278 public List<XmlValidationFinding> getFindings() {
279 return Collections.unmodifiableList(findings);
280 }
281
282 @Override
283 public Level getHighestSeverity() {
284 return highestSeverity;
285 }
286 }
287 }