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