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 "ProfileParser.java".  Description: 
10  "Parses a Message Profile XML document into a RuntimeProfile object." 
11  
12  The Initial Developer of the Original Code is University Health Network. Copyright (C) 
13  2003.  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  
28  package ca.uhn.hl7v2.conf.parser;
29  
30  import java.io.BufferedReader;
31  import java.io.File;
32  import java.io.FileNotFoundException;
33  import java.io.FileReader;
34  import java.io.IOException;
35  import java.io.InputStream;
36  
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  import org.w3c.dom.DOMError;
40  import org.w3c.dom.DOMErrorHandler;
41  import org.w3c.dom.Document;
42  import org.w3c.dom.Element;
43  import org.w3c.dom.Node;
44  import org.w3c.dom.NodeList;
45  
46  import ca.uhn.hl7v2.conf.ProfileException;
47  import ca.uhn.hl7v2.conf.spec.MetaData;
48  import ca.uhn.hl7v2.conf.spec.RuntimeProfile;
49  import ca.uhn.hl7v2.conf.spec.message.AbstractComponent;
50  import ca.uhn.hl7v2.conf.spec.message.AbstractSegmentContainer;
51  import ca.uhn.hl7v2.conf.spec.message.Component;
52  import ca.uhn.hl7v2.conf.spec.message.DataValue;
53  import ca.uhn.hl7v2.conf.spec.message.Field;
54  import ca.uhn.hl7v2.conf.spec.message.ProfileStructure;
55  import ca.uhn.hl7v2.conf.spec.message.Seg;
56  import ca.uhn.hl7v2.conf.spec.message.SegGroup;
57  import ca.uhn.hl7v2.conf.spec.message.StaticDef;
58  import ca.uhn.hl7v2.conf.spec.message.SubComponent;
59  import ca.uhn.hl7v2.util.XMLUtils;
60  
61  /**
62   * <p>
63   * Parses a Message Profile XML document into a RuntimeProfile object. A Message Profile is a formal
64   * description of additional constraints on a message (beyond what is specified in the HL7
65   * specification), usually for a particular system, region, etc. Message profiles are introduced in
66   * HL7 version 2.5 section 2.12. The RuntimeProfile object is simply an object representation of the
67   * profile, which may be used for validating messages or editing the profile.
68   * </p>
69   * <p>
70   * Example usage: <code><pre>
71   * 		// Load the profile from the classpath
72   *      ProfileParser parser = new ProfileParser(false);
73   *      RuntimeProfile profile = parser.parseClasspath("ca/uhn/hl7v2/conf/parser/example_ack.xml");
74   * 
75   *      // Create a message to validate
76   *      String message = "MSH|^~\\&|||||||ACK^A01|1|D|2.4|||||CAN|wrong|F^^HL70001^x^^HL78888|\r"; //note HL7888 doesn't exist
77   *      ACK msg = (ACK) (new PipeParser()).parse(message);
78   * 		
79   *      // Validate
80   * 		HL7Exception[] errors = new DefaultValidator().validate(msg, profile.getMessage());
81   * 		
82   * 		// Each exception is a validation error
83   * 		System.out.println("Validation errors: " + Arrays.asList(errors));
84   * </pre></code>
85   * </p>
86   * 
87   * @author Bryan Tripp
88   */
89  public class ProfileParser {
90  
91  	private static final String PROFILE_XSD = "ca/uhn/hl7v2/conf/parser/message_profile.xsd";
92  
93  	private static final Logger log = LoggerFactory.getLogger(ProfileParser.class);
94  
95  	private final boolean alwaysValidate;
96  	private final DOMErrorHandler errorHandler;
97  
98  	/**
99  	 * Creates a new instance of ProfileParser
100 	 * 
101 	 * @param alwaysValidate if true, validates all profiles against a local copy of the
102 	 *            profile XSD; if false, validates against declared grammar (if any)
103 	 */
104 	public ProfileParser(boolean alwaysValidate) {
105 
106 		this.alwaysValidate = alwaysValidate;
107 		this.errorHandler = error -> {
108             if (error.getSeverity() == DOMError.SEVERITY_WARNING) {
109                 log.warn("Warning: {}", error.getMessage());
110             } else {
111                 throw new RuntimeException((Exception) error.getRelatedException());
112             }
113             return true;
114         };
115 	}
116 
117 
118 	/**
119 	 * Parses an XML profile string into a RuntimeProfile object.
120 	 * 
121 	 * Input is a path pointing to a textual file on the classpath. Note that the file will be read
122 	 * using the thread context class loader.
123 	 * 
124 	 * For example, if you had a file called PROFILE.TXT in package com.foo.stuff, you would pass in
125 	 * "com/foo/stuff/PROFILE.TXT"
126 	 * 
127 	 * @throws IOException If the resource can't be read
128 	 */
129 	public RuntimeProfile parseClasspath(String classPath) throws ProfileException, IOException {
130 
131 		InputStream stream = Thread.currentThread().getContextClassLoader()
132 				.getResourceAsStream(classPath);
133 		if (stream == null) {
134 			throw new FileNotFoundException(classPath);
135 		}
136 
137 		StringBuilder profileString = new StringBuilder();
138 		byte[] buffer = new byte[1000];
139 		int bytesRead;
140 		while ((bytesRead = stream.read(buffer)) > 0) {
141 			profileString.append(new String(buffer, 0, bytesRead));
142 		}
143 
144 		RuntimeProfile profile = new RuntimeProfile();
145 		Document doc = parseIntoDOM(profileString.toString());
146 
147 		Element root = doc.getDocumentElement();
148 		profile.setHL7Version(root.getAttribute("HL7Version"));
149 
150 		// get static definition
151 		NodeList nl = root.getElementsByTagName("HL7v2xStaticDef");
152 		Element staticDef = (Element) nl.item(0);
153 		StaticDef sd = parseStaticProfile(staticDef);
154 		profile.setMessage(sd);
155 		return profile;
156 	}
157 
158 	/**
159 	 * Parses an XML profile string into a RuntimeProfile object.
160 	 */
161 	public RuntimeProfile parse(String profileString) throws ProfileException {
162 		RuntimeProfile profile = new RuntimeProfile();
163 		Document doc = parseIntoDOM(profileString);
164 
165 		Element root = doc.getDocumentElement();
166 		profile.setHL7Version(root.getAttribute("HL7Version"));
167 
168 		NodeList metadataList = root.getElementsByTagName("MetaData");
169 		if (metadataList.getLength() > 0) {
170 			Element metadata = (Element) metadataList.item(0);
171 			String name = metadata.getAttribute("Name");
172 			profile.setName(name);
173 		}
174 
175 		// get static definition
176 		NodeList nl = root.getElementsByTagName("HL7v2xStaticDef");
177 		Element staticDef = (Element) nl.item(0);
178 		StaticDef sd = parseStaticProfile(staticDef);
179 		profile.setMessage(sd);
180 		return profile;
181 	}
182 
183 	private StaticDef parseStaticProfile(Element elem) throws ProfileException {
184 		StaticDef message = new StaticDef();
185 		message.setMsgType(elem.getAttribute("MsgType"));
186 		message.setEventType(elem.getAttribute("EventType"));
187 		message.setMsgStructID(elem.getAttribute("MsgStructID"));
188 		message.setOrderControl(elem.getAttribute("OrderControl"));
189 		message.setEventDesc(elem.getAttribute("EventDesc"));
190 		message.setIdentifier(elem.getAttribute("Identifier"));
191 		message.setRole(elem.getAttribute("Role"));
192 
193 		Element md = getFirstElementByTagName("MetaData", elem);
194 		if (md != null)
195 			message.setMetaData(parseMetaData(md));
196 
197 		message.setImpNote(getValueOfFirstElement("ImpNote", elem));
198 		message.setDescription(getValueOfFirstElement("Description", elem));
199 		message.setReference(getValueOfFirstElement("Reference", elem));
200 
201 		parseChildren(message, elem);
202 		return message;
203 	}
204 
205 	/** Parses metadata element */
206 	private MetaData parseMetaData(Element elem) {
207 		log.debug("ProfileParser.parseMetaData() has been called ... note that this method does nothing.");
208 		return null;
209 	}
210 
211 	/**
212 	 * Parses children (i.e. segment groups, segments) of a segment group or message profile
213 	 */
214 	private void parseChildren(AbstractSegmentContainer parent, Element elem)
215 			throws ProfileException {
216 		int childIndex = 1;
217 		NodeList children = elem.getChildNodes();
218 		for (int i = 0; i < children.getLength(); i++) {
219 			Node n = children.item(i);
220 			if (n.getNodeType() == Node.ELEMENT_NODE) {
221 				Element child = (Element) n;
222 				if (child.getNodeName().equalsIgnoreCase("SegGroup")) {
223 					SegGroup group = parseSegmentGroupProfile(child);
224 					parent.setChild(childIndex++, group);
225 				} else if (child.getNodeName().equalsIgnoreCase("Segment")) {
226 					Seg segment = parseSegmentProfile(child);
227 					parent.setChild(childIndex++, segment);
228 				}
229 			}
230 		}
231 	}
232 
233 	/** Parses a segment group profile */
234 	private SegGroup parseSegmentGroupProfile(Element elem) throws ProfileException {
235 		SegGroup group = new SegGroup();
236 		log.debug("Parsing segment group profile: " + elem.getAttribute("Name"));
237 
238 		parseProfileStuctureData(group, elem);
239 
240 		parseChildren(group, elem);
241 		return group;
242 	}
243 
244 	/** Parses a segment profile */
245 	private Seg parseSegmentProfile(Element elem) throws ProfileException {
246 		Seg segment = new Seg();
247 		log.debug("Parsing segment profile: " + elem.getAttribute("Name"));
248 
249 		parseProfileStuctureData(segment, elem);
250 
251 		int childIndex = 1;
252 		NodeList children = elem.getChildNodes();
253 		for (int i = 0; i < children.getLength(); i++) {
254 			Node n = children.item(i);
255 			if (n.getNodeType() == Node.ELEMENT_NODE) {
256 				Element child = (Element) n;
257 				if (child.getNodeName().equalsIgnoreCase("Field")) {
258 					Field field = parseFieldProfile(child);
259 					segment.setField(childIndex++, field);
260 				}
261 			}
262 		}
263 
264 		return segment;
265 	}
266 
267 	/** Parse common data in profile structure (eg SegGroup, Segment) */
268 	private void parseProfileStuctureData(ProfileStructure struct, Element elem)
269 			throws ProfileException {
270 		struct.setName(elem.getAttribute("Name"));
271 		struct.setLongName(elem.getAttribute("LongName"));
272 		struct.setUsage(elem.getAttribute("Usage"));
273 		String min = elem.getAttribute("Min");
274 		String max = elem.getAttribute("Max");
275 		try {
276 			struct.setMin(Short.parseShort(min));
277 			if (max.indexOf('*') >= 0) {
278 				struct.setMax((short) -1);
279 			} else {
280 				struct.setMax(Short.parseShort(max));
281 			}
282 		} catch (NumberFormatException e) {
283 			throw new ProfileException("Min and max must be short integers: " + min + ", " + max, e);
284 		}
285 
286 		struct.setImpNote(getValueOfFirstElement("ImpNote", elem));
287 		struct.setDescription(getValueOfFirstElement("Description", elem));
288 		struct.setReference(getValueOfFirstElement("Reference", elem));
289 		struct.setPredicate(getValueOfFirstElement("Predicate", elem));
290 	}
291 
292 	/** Parses a field profile */
293 	private Field parseFieldProfile(Element elem) throws ProfileException {
294 		Field field = new Field();
295 		log.debug("  Parsing field profile: " + elem.getAttribute("Name"));
296 
297 		field.setUsage(elem.getAttribute("Usage"));
298 		String itemNo = elem.getAttribute("ItemNo");
299 		String min = elem.getAttribute("Min");
300 		String max = elem.getAttribute("Max");
301 
302 		try {
303 			if (itemNo.length() > 0) {
304 				field.setItemNo(Short.parseShort(itemNo));
305 			}
306 		} catch (NumberFormatException e) {
307 			throw new ProfileException("Invalid ItemNo: " + itemNo + "( for name "
308 					+ elem.getAttribute("Name") + ")", e);
309 		} // try-catch
310 
311 		try {
312 			field.setMin(Short.parseShort(min));
313 			if (max.indexOf('*') >= 0) {
314 				field.setMax((short) -1);
315 			} else {
316 				field.setMax(Short.parseShort(max));
317 			}
318 		} catch (NumberFormatException e) {
319 			throw new ProfileException("Min and max must be short integers: " + min + ", " + max, e);
320 		}
321 
322 		parseAbstractComponentData(field, elem);
323 
324 		int childIndex = 1;
325 		NodeList children = elem.getChildNodes();
326 		for (int i = 0; i < children.getLength(); i++) {
327 			Node n = children.item(i);
328 			if (n.getNodeType() == Node.ELEMENT_NODE) {
329 				Element child = (Element) n;
330 				if (child.getNodeName().equalsIgnoreCase("Component")) {
331 					Component comp = (Component) parseComponentProfile(child, false);
332 					field.setComponent(childIndex++, comp);
333 				}
334 			}
335 		}
336 
337 		return field;
338 	}
339 
340 	/** Parses a component profile */
341 	private AbstractComponent<?> parseComponentProfile(Element elem, boolean isSubComponent)
342 			throws ProfileException {
343 		AbstractComponent<?> comp;
344 		if (isSubComponent) {
345 			log.debug("      Parsing subcomp profile: " + elem.getAttribute("Name"));
346 			comp = new SubComponent();
347 		} else {
348 			log.debug("    Parsing comp profile: " + elem.getAttribute("Name"));
349 			comp = new Component();
350 
351 			int childIndex = 1;
352 			NodeList children = elem.getChildNodes();
353 			for (int i = 0; i < children.getLength(); i++) {
354 				Node n = children.item(i);
355 				if (n.getNodeType() == Node.ELEMENT_NODE) {
356 					Element child = (Element) n;
357 					if (child.getNodeName().equalsIgnoreCase("SubComponent")) {
358 						SubComponent subcomp = (SubComponent) parseComponentProfile(child, true);
359 						((Component) comp).setSubComponent(childIndex++, subcomp);
360 					}
361 				}
362 			}
363 		}
364 
365 		parseAbstractComponentData(comp, elem);
366 
367 		return comp;
368 	}
369 
370 	/**
371 	 * Parses common features of AbstractComponents (ie field, component, subcomponent)
372 	 */
373 	private void parseAbstractComponentData(AbstractComponent<?> comp, Element elem)
374 			throws ProfileException {
375 		comp.setName(elem.getAttribute("Name"));
376 		comp.setUsage(elem.getAttribute("Usage"));
377 		comp.setDatatype(elem.getAttribute("Datatype"));
378 		String length = elem.getAttribute("Length");
379 		if (length != null && length.length() > 0) {
380 			try {
381 				comp.setLength(Long.parseLong(length));
382 			} catch (NumberFormatException e) {
383 				throw new ProfileException("Length must be a long integer: " + length, e);
384 			}
385 		}
386 		comp.setConstantValue(elem.getAttribute("ConstantValue"));
387 		String table = elem.getAttribute("Table");
388 		if (table != null && table.length() > 0) {
389 			try {
390 				comp.setTable(table);
391 			} catch (NumberFormatException e) {
392 				throw new ProfileException("Table must be a short integer: " + table, e);
393 			}
394 		}
395 
396 		comp.setImpNote(getValueOfFirstElement("ImpNote", elem));
397 		comp.setDescription(getValueOfFirstElement("Description", elem));
398 		comp.setReference(getValueOfFirstElement("Reference", elem));
399 		comp.setPredicate(getValueOfFirstElement("Predicate", elem));
400 
401 		int dataValIndex = 0;
402 		NodeList children = elem.getChildNodes();
403 		for (int i = 0; i < children.getLength(); i++) {
404 			Node n = children.item(i);
405 			if (n.getNodeType() == Node.ELEMENT_NODE) {
406 				Element child = (Element) n;
407 				if (child.getNodeName().equalsIgnoreCase("DataValues")) {
408 					DataValue val = new DataValue();
409 					val.setExValue(child.getAttribute("ExValue"));
410 					comp.setDataValues(dataValIndex++, val);
411 				}
412 			}
413 		}
414 
415 	}
416 
417 	/** Parses profile string into DOM document */
418 	private Document parseIntoDOM(String profileString) throws ProfileException {
419 		try {
420 			Document doc = XMLUtils.parse(profileString, true);
421 			if (alwaysValidate)
422 				XMLUtils.validate(doc, PROFILE_XSD, errorHandler);
423 			return doc;
424 		} catch (Exception e) {
425 			throw new ProfileException("Exception parsing message profile: " + e.getMessage(), e);
426 		}
427 	}
428 
429 	/**
430 	 * Returns the first child element of the given parent that matches the given tag name. Returns
431 	 * null if no instance of the expected element is present.
432 	 */
433 	private Element getFirstElementByTagName(String name, Element parent) {
434 		NodeList nl = parent.getElementsByTagName(name);
435 		Element ret = null;
436 		if (nl.getLength() > 0) {
437 			ret = (Element) nl.item(0);
438 		}
439 		return ret;
440 	}
441 
442 	/**
443 	 * Gets the result of getFirstElementByTagName() and returns the value of that element.
444 	 */
445 	private String getValueOfFirstElement(String name, Element parent) throws ProfileException {
446 		Element el = getFirstElementByTagName(name, parent);
447 		String val = null;
448 		if (el != null) {
449 			try {
450 				Node n = el.getFirstChild();
451 				if (n.getNodeType() == Node.TEXT_NODE) {
452 					val = n.getNodeValue();
453 				}
454 			} catch (Exception e) {
455 				throw new ProfileException("Unable to get value of node " + name, e);
456 			}
457 		}
458 		return val;
459 	}
460 
461 	public static void main(String[] args) {
462 
463 		if (args.length != 1) {
464 			System.out.println("Usage: ProfileParser profile_file");
465 			System.exit(1);
466 		}
467 
468 		try {
469 			// File f = new
470 			// File("C:\\Documents and Settings\\bryan\\hapilocal\\hapi\\ca\\uhn\\hl7v2\\conf\\parser\\example_ack.xml");
471 			File f = new File(args[0]);
472 			@SuppressWarnings("resource")
473 			BufferedReader in = new BufferedReader(new FileReader(f));
474 			char[] cbuf = new char[(int) f.length()];
475 			in.read(cbuf, 0, (int) f.length());
476 			String xml = String.valueOf(cbuf);
477 			// System.out.println(xml);
478 
479 			ProfileParser pp = new ProfileParser(true);
480 			pp.parse(xml);
481 		} catch (Exception e) {
482 			e.printStackTrace();
483 		}
484 	}
485 
486 }