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}