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}