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 java.io.File;
030 import java.io.IOException;
031 import java.io.StringReader;
032 import java.util.ArrayList;
033 import java.util.List;
034
035 import javax.xml.parsers.DocumentBuilder;
036 import javax.xml.parsers.DocumentBuilderFactory;
037 import javax.xml.parsers.FactoryConfigurationError;
038 import javax.xml.parsers.ParserConfigurationException;
039
040 import org.apache.xerces.util.XMLGrammarPoolImpl;
041 import org.apache.xerces.xni.grammars.XMLGrammarPool;
042 import org.apache.xpath.XPathAPI;
043 import org.slf4j.Logger;
044 import org.slf4j.LoggerFactory;
045 import org.w3c.dom.DOMImplementation;
046 import org.w3c.dom.Document;
047 import org.w3c.dom.Element;
048 import org.w3c.dom.Node;
049 import org.xml.sax.InputSource;
050 import org.xml.sax.SAXException;
051 import org.xml.sax.SAXParseException;
052 import org.xml.sax.XMLReader;
053 import org.xml.sax.helpers.DefaultHandler;
054 import org.xml.sax.helpers.XMLReaderFactory;
055
056 import ca.uhn.hl7v2.validation.EncodingRule;
057 import 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")
064 public 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 }