001 /** 002 The 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. 004 You may obtain a copy of the License at http://www.mozilla.org/MPL/ 005 Software distributed under the License is distributed on an "AS IS" basis, 006 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 007 specific language governing rights and limitations under the License. 008 009 The Original Code is "XMLSchemaRule.java". Description: 010 "Validate hl7 v2.xml messages against a given xml-schema." 011 012 The Initial Developer of the Original Code is University Health Network. Copyright (C) 013 2004. All Rights Reserved. 014 015 Contributor(s): ______________________________________. 016 017 Alternatively, the contents of this file may be used under the terms of the 018 GNU General Public License (the ?GPL?), in which case the provisions of the GPL are 019 applicable instead of those above. If you wish to allow use of your version of this 020 file only under the terms of the GPL and not to allow others to use your version 021 of this file under the MPL, indicate your decision by deleting the provisions above 022 and replace them with the notice and other provisions required by the GPL License. 023 If you do not delete the provisions above, a recipient may use your version of 024 this file under either the MPL or the GPL. 025 */ 026 027 package ca.uhn.hl7v2.validation.impl; 028 029 import javax.xml.parsers.*; 030 import org.w3c.dom.*; 031 import org.w3c.dom.Document; 032 import org.apache.xpath.XPathAPI; 033 import org.xml.sax.InputSource; 034 import org.xml.sax.SAXException; 035 import org.xml.sax.SAXParseException; 036 import org.xml.sax.XMLReader; 037 import org.xml.sax.helpers.XMLReaderFactory; 038 import org.xml.sax.helpers.DefaultHandler; 039 import org.apache.xerces.xni.grammars.*; 040 import org.apache.xerces.util.*; 041 import java.util.ArrayList; 042 import java.util.List; 043 import java.io.IOException; 044 import java.io.StringReader; 045 import java.io.File; 046 047 import ca.uhn.hl7v2.validation.EncodingRule; 048 import ca.uhn.hl7v2.validation.ValidationException; 049 import ca.uhn.log.*; 050 051 /** 052 * <p>Validate hl7 version 2 messages encoded according to the HL7 XML Encoding Syntax against xml schemas provided by hl7.org</p> 053 * @author Nico Vannieuwenhuyze 054 */ 055 public class XMLSchemaRule implements EncodingRule { 056 057 private static final HapiLog log = HapiLogFactory.getHapiLog(XMLSchemaRule.class); 058 private static final String parserName = "org.apache.xerces.parsers.SAXParser"; 059 060 private XMLGrammarPool myGrammarPool = new XMLGrammarPoolImpl(); 061 private Element myNamespaceNode; 062 private DocumentBuilder myBuilder; 063 064 private class SchemaEventHandler extends DefaultHandler 065 { 066 private List validationErrors; 067 068 public SchemaEventHandler(List theValidationErrorList) 069 { 070 validationErrors = theValidationErrorList; 071 } 072 073 /** Warning. */ 074 public void warning(SAXParseException ex) { 075 076 validationErrors.add(new ValidationException("[Warning] "+ 077 getLocationString(ex)+": "+ 078 ex.getMessage() + " ")); 079 } 080 081 /** Error. */ 082 public void error(SAXParseException ex) { 083 084 validationErrors.add(new ValidationException("[Error] "+ 085 getLocationString(ex)+": "+ 086 ex.getMessage() + " ")); 087 } 088 089 /** Fatal error. */ 090 public void fatalError(SAXParseException ex) throws SAXException { 091 092 validationErrors.add(new ValidationException("[Fatal Error] "+ 093 getLocationString(ex)+": "+ 094 ex.getMessage() + " ")); 095 } 096 097 /** Returns a string of the location. */ 098 private String getLocationString(SAXParseException ex) { 099 StringBuffer str = new StringBuffer(); 100 101 String systemId = ex.getSystemId(); 102 if (systemId != null) { 103 int index = systemId.lastIndexOf('/'); 104 if (index != -1) 105 systemId = systemId.substring(index + 1); 106 str.append(systemId); 107 } 108 str.append(':'); 109 str.append(ex.getLineNumber()); 110 str.append(':'); 111 str.append(ex.getColumnNumber()); 112 113 return str.toString(); 114 115 } // getLocationString(SAXParseException):String 116 117 } 118 119 /** Creates a new instance of XMLSchemaValidator */ 120 public XMLSchemaRule() { 121 myBuilder = createDocumentBuilder(); 122 myNamespaceNode = createNamespaceNode(myBuilder); 123 } 124 125 /** 126 * <P>Test/validate a given xml document against a hl7 v2.xml schema.</P> 127 * <P>Before the schema is applied, the namespace is verified because otherwise schema validation fails anyway.</P> 128 * <P>If a schema file is specified in the xml message and the file can be located on the disk this one is used. 129 * If no schema has been specified, or the file can't be located, a system property ca.uhn.hl7v2.validation.xmlschemavalidator.schemalocation. + version 130 * can be used to assign a default schema location.</P> 131 * 132 * @param msg the xml message (as string) to be validated. 133 * @return ValidationException[] 134 */ 135 136 public ValidationException[] test(String msg) { 137 List validationErrors = new ArrayList(20); 138 Document domDocumentToValidate = null; 139 140 StringReader stringReaderForDom = new StringReader(msg); 141 try 142 { 143 // parse the icoming string into a dom document - no schema validation yet 144 domDocumentToValidate = myBuilder.parse(new InputSource(stringReaderForDom)); 145 146 // check if the xml document has the right default namespace 147 if (validateNamespace(domDocumentToValidate, validationErrors)) 148 { 149 String schemaLocation = getSchemaLocation(domDocumentToValidate, validationErrors); 150 if (schemaLocation.length() > 0) 151 { 152 // now parse the icoming string using a sax parser with schema validation 153 XMLReader parser = XMLReaderFactory.createXMLReader(parserName); 154 SchemaEventHandler eventHandler = new SchemaEventHandler(validationErrors); 155 parser.setContentHandler(eventHandler); 156 parser.setErrorHandler(eventHandler); 157 parser.setProperty("http://apache.org/xml/properties/schema/external-schemaLocation", "urn:hl7-org:v2xml" + " " + schemaLocation); 158 parser.setFeature("http://xml.org/sax/features/validation", true); 159 parser.setFeature("http://apache.org/xml/features/validation/schema", true); 160 parser.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true); 161 parser.setProperty("http://apache.org/xml/properties/internal/grammar-pool", myGrammarPool); 162 StringReader stringReaderForSax =new StringReader(msg); 163 parser.parse(new InputSource(stringReaderForSax)); 164 } 165 } 166 } 167 catch (SAXException se) 168 { 169 log.error("Unable to parse message - please verify that it's a valid xml document"); 170 log.error("SAXException: ", se); 171 validationErrors.add(new ValidationException("Unable to parse message - please verify that it's a valid xml document" + " [SAXException] " + se.getMessage())); 172 173 } 174 catch (IOException e) 175 { 176 log.error("Unable to parse message - please verify that it's a valid xml document"); 177 log.error("IOException: ", e); 178 validationErrors.add(new ValidationException("Unable to parse message - please verify that it's a valid xml document" + " [IOException] " + e.getMessage())); 179 } 180 181 return (ValidationException[]) validationErrors.toArray(new ValidationException[0]); 182 183 } 184 185 private Element createNamespaceNode(DocumentBuilder theBuilder) 186 { 187 Element namespaceNode = null; 188 // set up a document purely to hold the namespace mappings prefix-uri 189 // prefix used is hl7v2xml 190 if (theBuilder != null) 191 { 192 DOMImplementation impl = theBuilder.getDOMImplementation(); 193 Document namespaceHolder = impl.createDocument( 194 "http://namespaceuri.org", 195 "f:namespaceMapping", null); 196 namespaceNode = namespaceHolder.getDocumentElement(); 197 namespaceNode.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:hl7v2xml", 198 "urn:hl7-org:v2xml"); 199 namespaceNode.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); 200 } 201 return namespaceNode; 202 } 203 204 private DocumentBuilder createDocumentBuilder() 205 { 206 DocumentBuilder builder = null; 207 try 208 { 209 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 210 factory.setNamespaceAware(true); 211 212 try 213 { 214 builder = factory.newDocumentBuilder(); 215 } 216 catch (ParserConfigurationException e) 217 { 218 log.error(e.getMessage()); 219 } 220 } 221 catch (FactoryConfigurationError e) 222 { 223 log.error(e.getMessage()); 224 } 225 226 return builder; 227 } 228 229 private String getSchemaLocation(Document domDocumentToValidate, List validationErrors) { 230 boolean validSchemaInDocument = false; 231 String schemaLocation = new String(); 232 String schemaFilename = new String(); 233 234 // retrieve the schema specified in the document 235 try 236 { 237 log.debug("Trying to retrieve the schema defined in the xml document"); 238 Node schemaNode = XPathAPI.selectSingleNode(domDocumentToValidate, "//@xsi:schemaLocation" , myNamespaceNode); 239 if (schemaNode != null) 240 { 241 log.debug("Schema defined in document: " + schemaNode.getNodeValue()); 242 String schemaItems[] = schemaNode.getNodeValue().split(" "); 243 if (schemaItems.length == 2) 244 { 245 File myFile = new File(schemaItems[1].toString()); 246 if (myFile.exists()) 247 { 248 validSchemaInDocument = true; 249 schemaFilename = schemaItems[1].toString(); 250 log.debug("Schema defined in document points to a valid file - use this one"); 251 } 252 else 253 { 254 log.warn("Schema file defined in xml document not found on disk: " + schemaItems[1].toString()); 255 } 256 } 257 } 258 else 259 { 260 log.debug("No schema defined in the xml document"); 261 } 262 263 // if no valid schema was found - use the default (version dependent) from property 264 if (!validSchemaInDocument) 265 { 266 log.debug("Lookup hl7 version in msh-12 to know which default schema to use"); 267 Node versionNode = XPathAPI.selectSingleNode(domDocumentToValidate, "//hl7v2xml:MSH.12/hl7v2xml:VID.1/text()" , myNamespaceNode); 268 if (versionNode != null) 269 { 270 String schemaLocationProperty = new String("ca.uhn.hl7v2.validation.xmlschemavalidator.schemalocation.") + versionNode.getNodeValue(); 271 log.debug("Lookup schema location system property: " + schemaLocationProperty); 272 schemaLocation = System.getProperty(schemaLocationProperty); 273 if (schemaLocation == null) 274 { 275 log.warn("System property for schema location path " + schemaLocationProperty + " not defined"); 276 schemaLocation = System.getProperty("user.dir") + "\\v"+ versionNode.getNodeValue().replaceAll("\\.", "") + "\\xsd"; 277 log.info("Using default schema location path (current directory\\v2x\\xsd) " + schemaLocation); 278 } 279 280 // use the messagestructure as schema file name (root) 281 schemaFilename = schemaLocation + "/" + domDocumentToValidate.getDocumentElement().getNodeName() + ".xsd"; 282 File myFile = new File(schemaFilename); 283 if (myFile.exists()) 284 { 285 validSchemaInDocument = true; 286 log.debug("Valid schema file present: " + schemaFilename); 287 } 288 else 289 { 290 log.warn("Schema file not found on disk: " + schemaFilename); 291 } 292 } 293 else 294 { 295 log.error("HL7 version node MSH-12 not present - unable to determine default schema"); 296 } 297 } 298 } 299 catch (Exception e) 300 { 301 log.error(e.getMessage()); 302 } 303 304 if (validSchemaInDocument) 305 { 306 return schemaFilename; 307 } 308 else 309 { 310 ValidationException e = new ValidationException("Unable to retrieve a valid schema to use for message validation - please check logs"); 311 validationErrors.add(e); 312 return ""; 313 } 314 } 315 316 private boolean validateNamespace(Document domDocumentToValidate, List validationErrors) { 317 // start by verifying the default namespace if this isn't correct the rest will fail anyway 318 if (domDocumentToValidate.getDocumentElement().getNamespaceURI() == null) 319 { 320 ValidationException e = new ValidationException("The default namespace of the xml document is not specified - should be urn:hl7-org:v2xml"); 321 validationErrors.add(e); 322 log.error("The default namespace of the xml document is not specified - should be urn:hl7-org:v2xml"); 323 } 324 else 325 { 326 if (! domDocumentToValidate.getDocumentElement().getNamespaceURI().equals("urn:hl7-org:v2xml")) 327 { 328 ValidationException e = new ValidationException("The default namespace of the xml document (" + domDocumentToValidate.getDocumentElement().getNamespaceURI() + ") is incorrect - should be urn:hl7-org:v2xml"); 329 validationErrors.add(e); 330 log.error("The default namespace of the xml document (" + domDocumentToValidate.getDocumentElement().getNamespaceURI() + ") is incorrect - should be urn:hl7-org:v2xml"); 331 } 332 else 333 { 334 return true; 335 } 336 } 337 return false; 338 } 339 340 /** 341 * @see ca.uhn.hl7v2.validation.Rule#getDescription() 342 */ 343 public String getDescription() { 344 return "Checks that an encoded XML message validates against a declared or default schema " + 345 "(it is recommended to use the standard HL7 schema, but this is not enforced here)."; 346 } 347 348 /** 349 * @see ca.uhn.hl7v2.validation.Rule#getSectionReference() 350 */ 351 public String getSectionReference() { 352 return "http://www.hl7.org/Special/committees/xml/drafts/v2xml.html"; 353 } 354 355 356 }