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