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    }