001package ca.uhn.fhir.validation; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.ConfigurationException; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.rest.api.EncodingEnum; 026import ca.uhn.fhir.util.ClasspathUtil; 027import org.hl7.fhir.instance.model.api.IBaseResource; 028import org.w3c.dom.ls.LSInput; 029import org.w3c.dom.ls.LSResourceResolver; 030import org.xml.sax.SAXException; 031import org.xml.sax.SAXNotRecognizedException; 032import org.xml.sax.SAXParseException; 033 034import javax.xml.XMLConstants; 035import javax.xml.transform.Source; 036import javax.xml.transform.stream.StreamSource; 037import javax.xml.validation.Schema; 038import javax.xml.validation.SchemaFactory; 039import javax.xml.validation.Validator; 040import java.io.ByteArrayInputStream; 041import java.io.IOException; 042import java.io.StringReader; 043import java.util.Collections; 044import java.util.HashMap; 045import java.util.HashSet; 046import java.util.Map; 047import java.util.Set; 048 049public class SchemaBaseValidator implements IValidatorModule { 050 public static final String RESOURCES_JAR_NOTE = "Note that as of HAPI FHIR 1.2, DSTU2 validation files are kept in a separate JAR (hapi-fhir-validation-resources-XXX.jar) which must be added to your classpath. See the HAPI FHIR download page for more information."; 051 052 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class); 053 private static final Set<String> SCHEMA_NAMES; 054 private static boolean ourJaxp15Supported; 055 056 static { 057 HashSet<String> sn = new HashSet<>(); 058 sn.add("xml.xsd"); 059 sn.add("xhtml1-strict.xsd"); 060 sn.add("fhir-single.xsd"); 061 sn.add("fhir-xhtml.xsd"); 062 sn.add("tombstone.xsd"); 063 sn.add("opensearch.xsd"); 064 sn.add("opensearchscore.xsd"); 065 sn.add("xmldsig-core-schema.xsd"); 066 SCHEMA_NAMES = Collections.unmodifiableSet(sn); 067 } 068 069 private final Map<String, Schema> myKeyToSchema = new HashMap<>(); 070 private FhirContext myCtx; 071 072 public SchemaBaseValidator(FhirContext theContext) { 073 myCtx = theContext; 074 } 075 076 private void doValidate(IValidationContext<?> theContext) { 077 Schema schema = loadSchema(); 078 079 try { 080 Validator validator = schema.newValidator(); 081 MyErrorHandler handler = new MyErrorHandler(theContext); 082 validator.setErrorHandler(handler); 083 String encodedResource; 084 if (theContext.getResourceAsStringEncoding() == EncodingEnum.XML) { 085 encodedResource = theContext.getResourceAsString(); 086 } else { 087 encodedResource = theContext.getFhirContext().newXmlParser().encodeResourceToString((IBaseResource) theContext.getResource()); 088 } 089 090 try { 091 /* 092 * See https://github.com/hapifhir/hapi-fhir/issues/339 093 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing 094 */ 095 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 096 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 097 } catch (SAXNotRecognizedException ex) { 098 ourLog.debug("Jaxp 1.5 Support not found.", ex); 099 } 100 101 validator.validate(new StreamSource(new StringReader(encodedResource))); 102 } catch (SAXParseException e) { 103 SingleValidationMessage message = new SingleValidationMessage(); 104 message.setLocationLine(e.getLineNumber()); 105 message.setLocationCol(e.getColumnNumber()); 106 message.setMessage(e.getLocalizedMessage()); 107 message.setSeverity(ResultSeverityEnum.FATAL); 108 theContext.addValidationMessage(message); 109 } catch (SAXException | IOException e) { 110 // Catch all 111 throw new ConfigurationException("Could not load/parse schema file", e); 112 } 113 } 114 115 private Schema loadSchema() { 116 String key = "fhir-single.xsd"; 117 118 synchronized (myKeyToSchema) { 119 Schema schema = myKeyToSchema.get(key); 120 if (schema != null) { 121 return schema; 122 } 123 124 Source baseSource = loadXml("fhir-single.xsd"); 125 126 SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 127 schemaFactory.setResourceResolver(new MyResourceResolver()); 128 129 try { 130 try { 131 /* 132 * See https://github.com/hapifhir/hapi-fhir/issues/339 133 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing 134 */ 135 schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 136 ourJaxp15Supported = true; 137 } catch (SAXNotRecognizedException e) { 138 ourJaxp15Supported = false; 139 ourLog.warn("Jaxp 1.5 Support not found.", e); 140 } 141 schema = schemaFactory.newSchema(new Source[]{baseSource}); 142 } catch (SAXException e) { 143 throw new ConfigurationException("Could not load/parse schema file: " + "fhir-single.xsd", e); 144 } 145 myKeyToSchema.put(key, schema); 146 return schema; 147 } 148 } 149 150 Source loadXml(String theSchemaName) { 151 String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName; 152 ourLog.debug("Going to load resource: {}", pathToBase); 153 154 String contents = ClasspathUtil.loadResource(pathToBase, ClasspathUtil.withBom()); 155 return new StreamSource(new StringReader(contents), null); 156 } 157 158 @Override 159 public void validateResource(IValidationContext<IBaseResource> theContext) { 160 doValidate(theContext); 161 } 162 163 private final class MyResourceResolver implements LSResourceResolver { 164 private MyResourceResolver() { 165 } 166 167 @Override 168 public LSInput resolveResource(String theType, String theNamespaceURI, String thePublicId, String theSystemId, String theBaseURI) { 169 if (theSystemId != null && SCHEMA_NAMES.contains(theSystemId)) { 170 LSInputImpl input = new LSInputImpl(); 171 input.setPublicId(thePublicId); 172 input.setSystemId(theSystemId); 173 input.setBaseURI(theBaseURI); 174 String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSystemId; 175 176 ourLog.debug("Loading referenced schema file: " + pathToBase); 177 178 byte[] bytes = ClasspathUtil.loadResourceAsByteArray(pathToBase); 179 input.setByteStream(new ByteArrayInputStream(bytes)); 180 return input; 181 182 } 183 184 throw new ConfigurationException("Unknown schema: " + theSystemId); 185 } 186 } 187 188 private static class MyErrorHandler implements org.xml.sax.ErrorHandler { 189 190 private IValidationContext<?> myContext; 191 192 MyErrorHandler(IValidationContext<?> theContext) { 193 myContext = theContext; 194 } 195 196 private void addIssue(SAXParseException theException, ResultSeverityEnum theSeverity) { 197 SingleValidationMessage message = new SingleValidationMessage(); 198 message.setLocationLine(theException.getLineNumber()); 199 message.setLocationCol(theException.getColumnNumber()); 200 message.setMessage(theException.getLocalizedMessage()); 201 message.setSeverity(theSeverity); 202 myContext.addValidationMessage(message); 203 } 204 205 @Override 206 public void error(SAXParseException theException) { 207 addIssue(theException, ResultSeverityEnum.ERROR); 208 } 209 210 @Override 211 public void fatalError(SAXParseException theException) { 212 addIssue(theException, ResultSeverityEnum.FATAL); 213 } 214 215 @Override 216 public void warning(SAXParseException theException) { 217 addIssue(theException, ResultSeverityEnum.WARNING); 218 } 219 220 } 221 222 public static boolean isJaxp15Supported() { 223 return ourJaxp15Supported; 224 } 225 226}