
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.i18n.Msg; 026import ca.uhn.fhir.rest.api.EncodingEnum; 027import ca.uhn.fhir.util.ClasspathUtil; 028import org.hl7.fhir.instance.model.api.IBaseResource; 029import org.w3c.dom.ls.LSInput; 030import org.w3c.dom.ls.LSResourceResolver; 031import org.xml.sax.SAXException; 032import org.xml.sax.SAXNotRecognizedException; 033import org.xml.sax.SAXParseException; 034 035import javax.xml.XMLConstants; 036import javax.xml.transform.Source; 037import javax.xml.transform.stream.StreamSource; 038import javax.xml.validation.Schema; 039import javax.xml.validation.SchemaFactory; 040import javax.xml.validation.Validator; 041import java.io.ByteArrayInputStream; 042import java.io.IOException; 043import java.io.StringReader; 044import java.util.Collections; 045import java.util.HashMap; 046import java.util.HashSet; 047import java.util.Map; 048import java.util.Set; 049 050public class SchemaBaseValidator implements IValidatorModule { 051 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."; 052 053 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class); 054 private static final Set<String> SCHEMA_NAMES; 055 private static boolean ourJaxp15Supported; 056 057 static { 058 HashSet<String> sn = new HashSet<>(); 059 sn.add("xml.xsd"); 060 sn.add("xhtml1-strict.xsd"); 061 sn.add("fhir-single.xsd"); 062 sn.add("fhir-xhtml.xsd"); 063 sn.add("tombstone.xsd"); 064 sn.add("opensearch.xsd"); 065 sn.add("opensearchscore.xsd"); 066 sn.add("xmldsig-core-schema.xsd"); 067 SCHEMA_NAMES = Collections.unmodifiableSet(sn); 068 } 069 070 private final Map<String, Schema> myKeyToSchema = new HashMap<>(); 071 private FhirContext myCtx; 072 073 public SchemaBaseValidator(FhirContext theContext) { 074 myCtx = theContext; 075 } 076 077 private void doValidate(IValidationContext<?> theContext) { 078 Schema schema = loadSchema(); 079 080 try { 081 Validator validator = schema.newValidator(); 082 MyErrorHandler handler = new MyErrorHandler(theContext); 083 validator.setErrorHandler(handler); 084 String encodedResource; 085 if (theContext.getResourceAsStringEncoding() == EncodingEnum.XML) { 086 encodedResource = theContext.getResourceAsString(); 087 } else { 088 encodedResource = theContext.getFhirContext().newXmlParser().encodeResourceToString((IBaseResource) theContext.getResource()); 089 } 090 091 try { 092 /* 093 * See https://github.com/hapifhir/hapi-fhir/issues/339 094 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing 095 */ 096 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 097 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 098 } catch (SAXNotRecognizedException ex) { 099 ourLog.debug("Jaxp 1.5 Support not found.", ex); 100 } 101 102 validator.validate(new StreamSource(new StringReader(encodedResource))); 103 } catch (SAXParseException e) { 104 SingleValidationMessage message = new SingleValidationMessage(); 105 message.setLocationLine(e.getLineNumber()); 106 message.setLocationCol(e.getColumnNumber()); 107 message.setMessage(e.getLocalizedMessage()); 108 message.setSeverity(ResultSeverityEnum.FATAL); 109 theContext.addValidationMessage(message); 110 } catch (SAXException | IOException e) { 111 // Catch all 112 throw new ConfigurationException(Msg.code(1967) + "Could not load/parse schema file", e); 113 } 114 } 115 116 private Schema loadSchema() { 117 String key = "fhir-single.xsd"; 118 119 synchronized (myKeyToSchema) { 120 Schema schema = myKeyToSchema.get(key); 121 if (schema != null) { 122 return schema; 123 } 124 125 Source baseSource = loadXml("fhir-single.xsd"); 126 127 SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 128 schemaFactory.setResourceResolver(new MyResourceResolver()); 129 130 try { 131 try { 132 /* 133 * See https://github.com/hapifhir/hapi-fhir/issues/339 134 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing 135 */ 136 schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 137 ourJaxp15Supported = true; 138 } catch (SAXNotRecognizedException e) { 139 ourJaxp15Supported = false; 140 ourLog.warn("Jaxp 1.5 Support not found.", e); 141 } 142 schema = schemaFactory.newSchema(new Source[]{baseSource}); 143 } catch (SAXException e) { 144 throw new ConfigurationException(Msg.code(1968) + "Could not load/parse schema file: " + "fhir-single.xsd", e); 145 } 146 myKeyToSchema.put(key, schema); 147 return schema; 148 } 149 } 150 151 Source loadXml(String theSchemaName) { 152 String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName; 153 ourLog.debug("Going to load resource: {}", pathToBase); 154 155 String contents = ClasspathUtil.loadResource(pathToBase, ClasspathUtil.withBom()); 156 return new StreamSource(new StringReader(contents), null); 157 } 158 159 @Override 160 public void validateResource(IValidationContext<IBaseResource> theContext) { 161 doValidate(theContext); 162 } 163 164 private final class MyResourceResolver implements LSResourceResolver { 165 private MyResourceResolver() { 166 } 167 168 @Override 169 public LSInput resolveResource(String theType, String theNamespaceURI, String thePublicId, String theSystemId, String theBaseURI) { 170 if (theSystemId != null && SCHEMA_NAMES.contains(theSystemId)) { 171 LSInputImpl input = new LSInputImpl(); 172 input.setPublicId(thePublicId); 173 input.setSystemId(theSystemId); 174 input.setBaseURI(theBaseURI); 175 String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSystemId; 176 177 ourLog.debug("Loading referenced schema file: " + pathToBase); 178 179 byte[] bytes = ClasspathUtil.loadResourceAsByteArray(pathToBase); 180 input.setByteStream(new ByteArrayInputStream(bytes)); 181 return input; 182 183 } 184 185 throw new ConfigurationException(Msg.code(1969) + "Unknown schema: " + theSystemId); 186 } 187 } 188 189 private static class MyErrorHandler implements org.xml.sax.ErrorHandler { 190 191 private IValidationContext<?> myContext; 192 193 MyErrorHandler(IValidationContext<?> theContext) { 194 myContext = theContext; 195 } 196 197 private void addIssue(SAXParseException theException, ResultSeverityEnum theSeverity) { 198 SingleValidationMessage message = new SingleValidationMessage(); 199 message.setLocationLine(theException.getLineNumber()); 200 message.setLocationCol(theException.getColumnNumber()); 201 message.setMessage(theException.getLocalizedMessage()); 202 message.setSeverity(theSeverity); 203 myContext.addValidationMessage(message); 204 } 205 206 @Override 207 public void error(SAXParseException theException) { 208 addIssue(theException, ResultSeverityEnum.ERROR); 209 } 210 211 @Override 212 public void fatalError(SAXParseException theException) { 213 addIssue(theException, ResultSeverityEnum.FATAL); 214 } 215 216 @Override 217 public void warning(SAXParseException theException) { 218 addIssue(theException, ResultSeverityEnum.WARNING); 219 } 220 221 } 222 223 public static boolean isJaxp15Supported() { 224 return ourJaxp15Supported; 225 } 226 227}