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