View Javadoc
1   /**
2   The contents of this file are subject to the Mozilla Public License Version 1.1 
3   (the "License"); you may not use this file except in compliance with the License. 
4   You may obtain a copy of the License at http://www.mozilla.org/MPL/ 
5   Software distributed under the License is distributed on an "AS IS" basis, 
6   WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 
7   specific language governing rights and limitations under the License. 
8   
9   The Original Code is "XMLSchemaRule.java".  Description: 
10  "Validate hl7 v2.xml messages against a given xml-schema." 
11  
12  The Initial Developer of the Original Code is University Health Network. Copyright (C) 
13  2004.  All Rights Reserved. 
14  
15  Contributor(s): ______________________________________. 
16  
17  Alternatively, the contents of this file may be used under the terms of the 
18  GNU General Public License (the "GPL"), in which case the provisions of the GPL are 
19  applicable instead of those above.  If you wish to allow use of your version of this 
20  file only under the terms of the GPL and not to allow others to use your version 
21  of this file under the MPL, indicate your decision by deleting  the provisions above 
22  and replace  them with the notice and other provisions required by the GPL License.  
23  If you do not delete the provisions above, a recipient may use your version of 
24  this file under either the MPL or the GPL. 
25   */
26  
27  package ca.uhn.hl7v2.validation.impl;
28  
29  import java.io.File;
30  import java.io.IOException;
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.Map;
34  
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  import org.w3c.dom.DOMError;
38  import org.w3c.dom.DOMErrorHandler;
39  import org.w3c.dom.Document;
40  import org.w3c.dom.Element;
41  import org.w3c.dom.Node;
42  import org.w3c.dom.NodeList;
43  
44  import ca.uhn.hl7v2.Version;
45  import ca.uhn.hl7v2.util.XMLUtils;
46  import ca.uhn.hl7v2.validation.ValidationException;
47  
48  /**
49   * Validates HL7 version 2 messages encoded according to the HL7 XML Encoding Syntax against XML
50   * schemas provided by hl7.org.
51   * <p>
52   * The XML schema to validate against is determined as follows:
53   * <ul>
54   * <li>if the XML document contains a schemaLocation that points to a valid file, this file is
55   * assumed to contain the schema definition
56   * <li>the location configured using {@link #setSchemaLocations(Map)}
57   * </ul>
58   * <p>
59   * The validation fails, if
60   * <ul>
61   * <li>no valid schema file could be found
62   * <li>the default namespace of the XML document is not <code>urn:hl7-org:v2xml</code>
63   * <li>the document does not validate against the XML schema file foudn as described above
64   * </ul>
65   * 
66   * @author Nico Vannieuwenhuyze
67   * @author Christian Ohr
68   */
69  @SuppressWarnings("serial")
70  public class XMLSchemaRule extends AbstractEncodingRule {
71  
72  	private static final String SECTION_REFERENCE = "http://www.hl7.org/Special/committees/xml/drafts/v2xml.html";
73  	private static final String DESCRIPTION = "Checks that an encoded XML message validates against a declared or default schema "
74  			+ "(it is recommended to use the standard HL7 schema, but this is not enforced here).";
75  	private static final Logger log = LoggerFactory.getLogger(XMLSchemaRule.class);
76  	private static final String DEFAULT_NS = "urn:hl7-org:v2xml";
77  
78  	private Map<String, String> locations;
79  
80  	private static class ErrorHandler implements DOMErrorHandler {
81  		private final List<ValidationException> validationErrors;
82  
83  		public ErrorHandler(List<ValidationException> validationErrors) {
84  			super();
85  			this.validationErrors = validationErrors;
86  		}
87  
88  		public boolean handleError(DOMError error) {
89  			validationErrors.add(new ValidationException(getSeverity(error) + error.getMessage()));
90  			return true;
91  		}
92  
93  		private String getSeverity(DOMError error) {
94  			switch (error.getSeverity()) {
95  			case DOMError.SEVERITY_WARNING:
96  				return "WARNING: ";
97  			case DOMError.SEVERITY_ERROR:
98  				return "ERROR: ";
99  			default:
100 				return "FATAL ERROR: ";
101 			}
102 		}
103 
104 	}
105 
106 	/**
107 	 * Test/validate a given xml document against a hl7 v2.xml schema.
108 	 * <p>
109 	 * Before the schema is applied, the namespace is verified because otherwise schema validation
110 	 * fails anyway.
111 	 * <p>
112 	 * If a schema file is specified in the xml message and the file can be located on the disk this
113 	 * one is used. If no schema has been specified, or the file can't be located, the locations
114 	 * property is used.
115 	 * 
116 	 * @param msg the xml message (as string) to be validated.
117 	 * @return ValidationException[] an array of validation exceptions, which is zero-sized when no
118 	 *         validation errors occured.
119 	 */
120 	public ValidationException[] apply(String msg) {
121 		List<ValidationException> validationErrors = new ArrayList<>();
122 		try {
123 			// parse the incoming string into a dom document - no schema validation yet
124 			Document doc = XMLUtils.parse(msg);
125 			if (hasCorrectNamespace(doc, validationErrors)) {
126 				XMLUtils.validate(doc, getSchemaLocation(doc), new ErrorHandler(validationErrors));
127 			}
128 		} catch (Exception e) {
129 			log.error("Unable to validate message: {}", e.getMessage(), e);
130 			validationErrors.add(new ValidationException("Unable to validate message "
131 					+ e.getMessage(), e));
132 		}
133 
134 		return validationErrors.toArray(new ValidationException[0]);
135 
136 	}
137 
138 	/**
139 	 * 
140 	 * Try to obtain the XML schema file (depending on message version), either as provided in
141 	 * xsi:schemaLocation, or as provided in the locations property or in a subdirectory of the
142 	 * current dir.
143 	 * 
144 	 * @param doc the DOM document
145 	 * @return the file name of the schema
146 	 * @throws IOException
147 	 */
148 	private String getSchemaLocation(Document doc) throws IOException {
149 		String schemaFilename = extractSchemaLocation(doc);
150 		if (schemaFilename == null) {
151 			if ((schemaFilename = staticSchema(doc)) == null) {
152 				throw new IOException(
153 						"Unable to retrieve a valid schema to use for message validation");
154 			}
155 		}
156 		return schemaFilename;
157 
158 	}
159 
160 	private String extractSchemaLocation(Document doc) {
161 		String schemaFileName = null;
162 		log.debug("Trying to retrieve the schema defined in the xml document");
163 		Element element = doc.getDocumentElement();
164 		String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance",
165 				"schemaLocation");
166 		if (schemaLocation.length() > 0) {
167 			log.debug("Schema defined in document: {}", schemaLocation);
168 			String[] schemaItems = schemaLocation.split(" ");
169 			if (schemaItems.length == 2) {
170 				File f = new File(schemaItems[1]);
171 				if (f.exists()) {
172 					schemaFileName = schemaItems[1];
173 					log.debug("Schema defined in document points to a valid file");
174 				} else {
175 					log.warn("Schema file defined in xml document not found on disk: {}",
176 							schemaItems[1]);
177 				}
178 			}
179 		} else {
180 			log.debug("No schema location defined in the xml document");
181 		}
182 
183 		return schemaFileName;
184 	}
185 
186 	private String staticSchema(Document doc) {
187 		String schemaFilename = null;
188 		log.debug("Lookup HL7 version in MSH-12 to know which default schema to use");
189 		NodeList nodeList = doc.getElementsByTagNameNS(DEFAULT_NS, "VID.1");
190 		if (nodeList.getLength() == 1) {
191 			Node versionNode = nodeList.item(0);
192 			Version version = Version.versionOf(versionNode.getFirstChild().getNodeValue());
193 			String schemaLocation = locations.get(version.getVersion());
194 
195 			// use the message structure as schema file name (root)
196 			schemaFilename = schemaLocation + "/" + doc.getDocumentElement().getNodeName() + ".xsd";
197 			File myFile = new File(schemaFilename);
198 			if (myFile.exists()) {
199 				log.debug("Valid schema file present: {}", schemaFilename);
200 			} else {
201 				log.warn("Schema file not found on disk: {}", schemaFilename);
202 				schemaFilename = null;
203 			}
204 		} else {
205 			log.error("HL7 version node MSH-12 not present - unable to determine default schema");
206 		}
207 		return schemaFilename;
208 	}
209 
210 	/**
211 	 * @return <code>true</code> if default namespace is set properly
212 	 */
213 	private boolean hasCorrectNamespace(Document domDocumentToValidate,
214 			List<ValidationException> validationErrors) {
215 		String nsUri = domDocumentToValidate.getDocumentElement().getNamespaceURI();
216 		boolean ok = DEFAULT_NS.equals(nsUri);
217 		if (!ok) {
218 			ValidationException e = new ValidationException(
219 					"The default namespace of the XML document is incorrect - should be "
220 							+ DEFAULT_NS + " but was " + nsUri);
221 			validationErrors.add(e);
222 			log.error(e.getMessage());
223 		}
224 		return ok;
225 	}
226 
227 	public void setSchemaLocations(Map<String, String> locations) {
228 		this.locations = locations;
229 	}
230 
231 	Map<String, String> getSchemaLocations() {
232 		return locations;
233 	}
234 
235 	/**
236 	 * @see ca.uhn.hl7v2.validation.Rule#getDescription()
237 	 */
238 	public String getDescription() {
239 		return DESCRIPTION;
240 	}
241 
242 	/**
243 	 * @see ca.uhn.hl7v2.validation.Rule#getSectionReference()
244 	 */
245 	public String getSectionReference() {
246 		return SECTION_REFERENCE;
247 	}
248 
249 }