001/**
002The contents of this file are subject to the Mozilla Public License Version 1.1 
003(the "License"); you may not use this file except in compliance with the License. 
004You may obtain a copy of the License at http://www.mozilla.org/MPL/ 
005Software distributed under the License is distributed on an "AS IS" basis, 
006WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 
007specific language governing rights and limitations under the License. 
008
009The Original Code is "XMLSchemaRule.java".  Description: 
010"Validate hl7 v2.xml messages against a given xml-schema." 
011
012The Initial Developer of the Original Code is University Health Network. Copyright (C) 
0132004.  All Rights Reserved. 
014
015Contributor(s): ______________________________________. 
016
017Alternatively, the contents of this file may be used under the terms of the 
018GNU General Public License (the "GPL"), in which case the provisions of the GPL are 
019applicable instead of those above.  If you wish to allow use of your version of this 
020file only under the terms of the GPL and not to allow others to use your version 
021of this file under the MPL, indicate your decision by deleting  the provisions above 
022and replace  them with the notice and other provisions required by the GPL License.  
023If you do not delete the provisions above, a recipient may use your version of 
024this file under either the MPL or the GPL. 
025 */
026
027package ca.uhn.hl7v2.validation.impl;
028
029import java.io.File;
030import java.io.IOException;
031import java.util.ArrayList;
032import java.util.List;
033import java.util.Map;
034
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037import org.w3c.dom.DOMError;
038import org.w3c.dom.DOMErrorHandler;
039import org.w3c.dom.Document;
040import org.w3c.dom.Element;
041import org.w3c.dom.Node;
042import org.w3c.dom.NodeList;
043
044import ca.uhn.hl7v2.Version;
045import ca.uhn.hl7v2.util.XMLUtils;
046import ca.uhn.hl7v2.validation.ValidationException;
047
048/**
049 * Validates HL7 version 2 messages encoded according to the HL7 XML Encoding Syntax against XML
050 * schemas provided by hl7.org.
051 * <p>
052 * The XML schema to validate against is determined as follows:
053 * <ul>
054 * <li>if the XML document contains a schemaLocation that points to a valid file, this file is
055 * assumed to contain the schema definition
056 * <li>the location configured using {@link #setSchemaLocations(Map)}
057 * </ul>
058 * <p>
059 * The validation fails, if
060 * <ul>
061 * <li>no valid schema file could be found
062 * <li>the default namespace of the XML document is not <code>urn:hl7-org:v2xml</code>
063 * <li>the document does not validate against the XML schema file foudn as described above
064 * </ul>
065 * 
066 * @author Nico Vannieuwenhuyze
067 * @author Christian Ohr
068 */
069@SuppressWarnings("serial")
070public class XMLSchemaRule extends AbstractEncodingRule {
071
072        private static final String SECTION_REFERENCE = "http://www.hl7.org/Special/committees/xml/drafts/v2xml.html";
073        private static final String DESCRIPTION = "Checks that an encoded XML message validates against a declared or default schema "
074                        + "(it is recommended to use the standard HL7 schema, but this is not enforced here).";
075        private static final Logger log = LoggerFactory.getLogger(XMLSchemaRule.class);
076        private static final String DEFAULT_NS = "urn:hl7-org:v2xml";
077
078        private Map<String, String> locations;
079
080        private static class ErrorHandler implements DOMErrorHandler {
081                private List<ValidationException> validationErrors;
082
083                public ErrorHandler(List<ValidationException> validationErrors) {
084                        super();
085                        this.validationErrors = validationErrors;
086                }
087
088                public boolean handleError(DOMError error) {
089                        validationErrors.add(new ValidationException(getSeverity(error) + error.getMessage()));
090                        return true;
091                }
092
093                private String getSeverity(DOMError error) {
094                        switch (error.getSeverity()) {
095                        case DOMError.SEVERITY_WARNING:
096                                return "WARNING: ";
097                        case DOMError.SEVERITY_ERROR:
098                                return "ERROR: ";
099                        default:
100                                return "FATAL ERROR: ";
101                        }
102                }
103
104        }
105
106        /**
107         * Test/validate a given xml document against a hl7 v2.xml schema.
108         * <p>
109         * Before the schema is applied, the namespace is verified because otherwise schema validation
110         * fails anyway.
111         * <p>
112         * If a schema file is specified in the xml message and the file can be located on the disk this
113         * one is used. If no schema has been specified, or the file can't be located, the locations
114         * property is used.
115         * 
116         * @param msg the xml message (as string) to be validated.
117         * @return ValidationException[] an array of validation exceptions, which is zero-sized when no
118         *         validation errors occured.
119         */
120        public ValidationException[] apply(String msg) {
121                List<ValidationException> validationErrors = new ArrayList<ValidationException>();
122                try {
123                        // parse the incoming string into a dom document - no schema validation yet
124                        Document doc = XMLUtils.parse(msg);
125                        if (hasCorrectNamespace(doc, validationErrors)) {
126                                XMLUtils.validate(doc, getSchemaLocation(doc), new ErrorHandler(validationErrors));
127                        }
128                } catch (Exception e) {
129                        log.error("Unable to validate message: {}", e.getMessage(), e);
130                        validationErrors.add(new ValidationException("Unable to validate message "
131                                        + e.getMessage(), e));
132                }
133
134                return validationErrors.toArray(new ValidationException[validationErrors.size()]);
135
136        }
137
138        /**
139         * 
140         * Try to obtain the XML schema file (depending on message version), either as provided in
141         * xsi:schemaLocation, or as provided in the locations property or in a subdirectory of the
142         * current dir.
143         * 
144         * @param doc the DOM document
145         * @return the file name of the schema
146         * @throws IOException
147         */
148        private String getSchemaLocation(Document doc) throws IOException {
149                String schemaFilename = extractSchemaLocation(doc);
150                if (schemaFilename == null) {
151                        if ((schemaFilename = staticSchema(doc)) == null) {
152                                throw new IOException(
153                                                "Unable to retrieve a valid schema to use for message validation");
154                        }
155                }
156                return schemaFilename;
157
158        }
159
160        private String extractSchemaLocation(Document doc) {
161                String schemaFileName = null;
162                log.debug("Trying to retrieve the schema defined in the xml document");
163                Element element = doc.getDocumentElement();
164                String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance",
165                                "schemaLocation");
166                if (schemaLocation.length() > 0) {
167                        log.debug("Schema defined in document: {}", schemaLocation);
168                        String schemaItems[] = schemaLocation.split(" ");
169                        if (schemaItems.length == 2) {
170                                File f = new File(schemaItems[1]);
171                                if (f.exists()) {
172                                        schemaFileName = schemaItems[1];
173                                        log.debug("Schema defined in document points to a valid file");
174                                } else {
175                                        log.warn("Schema file defined in xml document not found on disk: {}",
176                                                        schemaItems[1]);
177                                }
178                        }
179                } else {
180                        log.debug("No schema location defined in the xml document");
181                }
182
183                return schemaFileName;
184        }
185
186        private String staticSchema(Document doc) {
187                String schemaFilename = null;
188                log.debug("Lookup HL7 version in MSH-12 to know which default schema to use");
189                NodeList nodeList = doc.getElementsByTagNameNS(DEFAULT_NS, "VID.1");
190                if (nodeList.getLength() == 1) {
191                        Node versionNode = nodeList.item(0);
192                        Version version = Version.versionOf(versionNode.getFirstChild().getNodeValue());
193                        String schemaLocation = locations.get(version.getVersion());
194
195                        // use the message structure as schema file name (root)
196                        schemaFilename = schemaLocation + "/" + doc.getDocumentElement().getNodeName() + ".xsd";
197                        File myFile = new File(schemaFilename);
198                        if (myFile.exists()) {
199                                log.debug("Valid schema file present: {}", schemaFilename);
200                        } else {
201                                log.warn("Schema file not found on disk: {}", schemaFilename);
202                                schemaFilename = null;
203                        }
204                } else {
205                        log.error("HL7 version node MSH-12 not present - unable to determine default schema");
206                }
207                return schemaFilename;
208        }
209
210        /**
211         * @return <code>true</code> if default namespace is set properly
212         */
213        private boolean hasCorrectNamespace(Document domDocumentToValidate,
214                        List<ValidationException> validationErrors) {
215                String nsUri = domDocumentToValidate.getDocumentElement().getNamespaceURI();
216                boolean ok = DEFAULT_NS.equals(nsUri);
217                if (!ok) {
218                        ValidationException e = new ValidationException(
219                                        "The default namespace of the XML document is incorrect - should be "
220                                                        + DEFAULT_NS + " but was " + nsUri);
221                        validationErrors.add(e);
222                        log.error(e.getMessage());
223                }
224                return ok;
225        }
226
227        public void setSchemaLocations(Map<String, String> locations) {
228                this.locations = locations;
229        }
230
231        Map<String, String> getSchemaLocations() {
232                return locations;
233        }
234
235        /**
236         * @see ca.uhn.hl7v2.validation.Rule#getDescription()
237         */
238        public String getDescription() {
239                return DESCRIPTION;
240        }
241
242        /**
243         * @see ca.uhn.hl7v2.validation.Rule#getSectionReference()
244         */
245        public String getSectionReference() {
246                return SECTION_REFERENCE;
247        }
248
249}