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    }