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