View Javadoc
1   package ca.uhn.fhir.validation;
2   
3   /*
4    * #%L
5    * HAPI FHIR - Core Library
6    * %%
7    * Copyright (C) 2014 - 2018 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  import java.io.*;
23  import java.nio.charset.Charset;
24  import java.util.*;
25  
26  import javax.xml.XMLConstants;
27  import javax.xml.transform.Source;
28  import javax.xml.transform.stream.StreamSource;
29  import javax.xml.validation.*;
30  
31  import org.apache.commons.io.IOUtils;
32  import org.apache.commons.io.input.BOMInputStream;
33  import org.hl7.fhir.instance.model.api.IBaseResource;
34  import org.w3c.dom.ls.LSInput;
35  import org.w3c.dom.ls.LSResourceResolver;
36  import org.xml.sax.*;
37  
38  import ca.uhn.fhir.context.ConfigurationException;
39  import ca.uhn.fhir.context.FhirContext;
40  import ca.uhn.fhir.rest.api.EncodingEnum;
41  import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
42  
43  public class SchemaBaseValidator implements IValidatorModule {
44  	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.";
45  
46  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class);
47  	private static final Set<String> SCHEMA_NAMES;
48  
49  	static {
50  		HashSet<String> sn = new HashSet<String>();
51  		sn.add("xml.xsd");
52  		sn.add("xhtml1-strict.xsd");
53  		sn.add("fhir-single.xsd");
54  		sn.add("fhir-xhtml.xsd");
55  		sn.add("tombstone.xsd");
56  		sn.add("opensearch.xsd");
57  		sn.add("opensearchscore.xsd");
58  		sn.add("xmldsig-core-schema.xsd");
59  		SCHEMA_NAMES = Collections.unmodifiableSet(sn);
60  	}
61  
62  	private Map<String, Schema> myKeyToSchema = new HashMap<String, Schema>();
63  	private FhirContext myCtx;
64  
65  	public SchemaBaseValidator(FhirContext theContext) {
66  		myCtx = theContext;
67  	}
68  
69  	private void doValidate(IValidationContext<?> theContext, String schemaName) {
70  		Schema schema = loadSchema("dstu", schemaName);
71  
72  		try {
73  			Validator validator = schema.newValidator();
74  			MyErrorHandler handler = new MyErrorHandler(theContext);
75  			validator.setErrorHandler(handler);
76  			String encodedResource;
77  			if (theContext.getResourceAsStringEncoding() == EncodingEnum.XML) {
78  				encodedResource = theContext.getResourceAsString();
79  			} else {
80  				encodedResource = theContext.getFhirContext().newXmlParser().encodeResourceToString((IBaseResource) theContext.getResource());
81  			}
82  
83  			try {
84  			/*
85  			 * See https://github.com/jamesagnew/hapi-fhir/issues/339
86  			 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
87  			 */
88  				validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
89  				validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
90  			}catch (SAXNotRecognizedException ex){
91  				ourLog.warn("Jaxp 1.5 Support not found.",ex);
92  			}
93  
94  			validator.validate(new StreamSource(new StringReader(encodedResource)));
95  		} catch (SAXParseException e) {
96  			SingleValidationMessage message = new SingleValidationMessage();
97  			message.setLocationLine(e.getLineNumber());
98  			message.setLocationCol(e.getColumnNumber());
99  			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 }