001/* 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.validation; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.i18n.Msg; 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 java.io.ByteArrayInputStream; 035import java.io.IOException; 036import java.io.StringReader; 037import java.util.Collections; 038import java.util.HashMap; 039import java.util.HashSet; 040import java.util.Map; 041import java.util.Set; 042import javax.xml.XMLConstants; 043import javax.xml.transform.Source; 044import javax.xml.transform.stream.StreamSource; 045import javax.xml.validation.Schema; 046import javax.xml.validation.SchemaFactory; 047import javax.xml.validation.Validator; 048 049public class SchemaBaseValidator implements IValidatorModule { 050 public static final String RESOURCES_JAR_NOTE = 051 "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) 089 theContext.getResource()); 090 } 091 092 try { 093 /* 094 * See https://github.com/hapifhir/hapi-fhir/issues/339 095 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing 096 */ 097 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 098 validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 099 } catch (SAXNotRecognizedException ex) { 100 ourLog.debug("Jaxp 1.5 Support not found.", ex); 101 } 102 103 validator.validate(new StreamSource(new StringReader(encodedResource))); 104 } catch (SAXParseException e) { 105 SingleValidationMessage message = new SingleValidationMessage(); 106 message.setLocationLine(e.getLineNumber()); 107 message.setLocationCol(e.getColumnNumber()); 108 message.setMessage(e.getLocalizedMessage()); 109 message.setSeverity(ResultSeverityEnum.FATAL); 110 theContext.addValidationMessage(message); 111 } catch (SAXException | IOException e) { 112 // Catch all 113 throw new ConfigurationException(Msg.code(1967) + "Could not load/parse schema file", e); 114 } 115 } 116 117 private Schema loadSchema() { 118 String key = "fhir-single.xsd"; 119 120 synchronized (myKeyToSchema) { 121 Schema schema = myKeyToSchema.get(key); 122 if (schema != null) { 123 return schema; 124 } 125 126 Source baseSource = loadXml("fhir-single.xsd"); 127 128 SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 129 schemaFactory.setResourceResolver(new MyResourceResolver()); 130 131 try { 132 try { 133 /* 134 * See https://github.com/hapifhir/hapi-fhir/issues/339 135 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing 136 */ 137 schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 138 ourJaxp15Supported = true; 139 } catch (SAXNotRecognizedException e) { 140 ourJaxp15Supported = false; 141 ourLog.warn("Jaxp 1.5 Support not found.", e); 142 } 143 schema = schemaFactory.newSchema(new Source[] {baseSource}); 144 } catch (SAXException e) { 145 throw new ConfigurationException( 146 Msg.code(1968) + "Could not load/parse schema file: " + "fhir-single.xsd", e); 147 } 148 myKeyToSchema.put(key, schema); 149 return schema; 150 } 151 } 152 153 Source loadXml(String theSchemaName) { 154 String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName; 155 ourLog.debug("Going to load resource: {}", pathToBase); 156 157 String contents = ClasspathUtil.loadResource(pathToBase, ClasspathUtil.withBom()); 158 return new StreamSource(new StringReader(contents), null); 159 } 160 161 @Override 162 public void validateResource(IValidationContext<IBaseResource> theContext) { 163 doValidate(theContext); 164 } 165 166 private final class MyResourceResolver implements LSResourceResolver { 167 private MyResourceResolver() {} 168 169 @Override 170 public LSInput resolveResource( 171 String theType, String theNamespaceURI, String thePublicId, String theSystemId, String theBaseURI) { 172 if (theSystemId != null && SCHEMA_NAMES.contains(theSystemId)) { 173 LSInputImpl input = new LSInputImpl(); 174 input.setPublicId(thePublicId); 175 input.setSystemId(theSystemId); 176 input.setBaseURI(theBaseURI); 177 String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSystemId; 178 179 ourLog.debug("Loading referenced schema file: " + pathToBase); 180 181 byte[] bytes = ClasspathUtil.loadResourceAsByteArray(pathToBase); 182 input.setByteStream(new ByteArrayInputStream(bytes)); 183 return input; 184 } 185 186 throw new ConfigurationException(Msg.code(1969) + "Unknown schema: " + theSystemId); 187 } 188 } 189 190 private static class MyErrorHandler implements org.xml.sax.ErrorHandler { 191 192 private IValidationContext<?> myContext; 193 194 MyErrorHandler(IValidationContext<?> theContext) { 195 myContext = theContext; 196 } 197 198 private void addIssue(SAXParseException theException, ResultSeverityEnum theSeverity) { 199 SingleValidationMessage message = new SingleValidationMessage(); 200 message.setLocationLine(theException.getLineNumber()); 201 message.setLocationCol(theException.getColumnNumber()); 202 message.setMessage(theException.getLocalizedMessage()); 203 message.setSeverity(theSeverity); 204 myContext.addValidationMessage(message); 205 } 206 207 @Override 208 public void error(SAXParseException theException) { 209 addIssue(theException, ResultSeverityEnum.ERROR); 210 } 211 212 @Override 213 public void fatalError(SAXParseException theException) { 214 addIssue(theException, ResultSeverityEnum.FATAL); 215 } 216 217 @Override 218 public void warning(SAXParseException theException) { 219 addIssue(theException, ResultSeverityEnum.WARNING); 220 } 221 } 222 223 public static boolean isJaxp15Supported() { 224 return ourJaxp15Supported; 225 } 226}