001package ca.uhn.fhir.validation;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2019 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                                        throw new InternalErrorException("Schema file not found: " + pathToBase);
216                                }
217
218                                input.setByteStream(baseIs);
219                                //FIXME resource leak
220                                return input;
221
222                        }
223
224                        throw new ConfigurationException("Unknown schema: " + theSystemId);
225                }
226        }
227
228}