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 "ProfileParser.java". Description:
010 "Parses a Message Profile XML document into a RuntimeProfile object."
011
012 The Initial Developer of the Original Code is University Health Network. Copyright (C)
013 2003. 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
028 package ca.uhn.hl7v2.conf.parser;
029
030 import java.io.BufferedReader;
031 import java.io.File;
032 import java.io.FileNotFoundException;
033 import java.io.FileReader;
034 import java.io.IOException;
035 import java.io.InputStream;
036 import java.io.InputStreamReader;
037 import java.io.StringReader;
038
039 import org.apache.xerces.parsers.DOMParser;
040 import org.apache.xerces.parsers.StandardParserConfiguration;
041 import org.slf4j.Logger;
042 import org.slf4j.LoggerFactory;
043 import org.w3c.dom.Document;
044 import org.w3c.dom.Element;
045 import org.w3c.dom.Node;
046 import org.w3c.dom.NodeList;
047 import org.xml.sax.EntityResolver;
048 import org.xml.sax.ErrorHandler;
049 import org.xml.sax.InputSource;
050 import org.xml.sax.SAXException;
051 import org.xml.sax.SAXParseException;
052
053 import ca.uhn.hl7v2.conf.ProfileException;
054 import ca.uhn.hl7v2.conf.spec.MetaData;
055 import ca.uhn.hl7v2.conf.spec.RuntimeProfile;
056 import ca.uhn.hl7v2.conf.spec.message.AbstractComponent;
057 import ca.uhn.hl7v2.conf.spec.message.AbstractSegmentContainer;
058 import ca.uhn.hl7v2.conf.spec.message.Component;
059 import ca.uhn.hl7v2.conf.spec.message.DataValue;
060 import ca.uhn.hl7v2.conf.spec.message.Field;
061 import ca.uhn.hl7v2.conf.spec.message.ProfileStructure;
062 import ca.uhn.hl7v2.conf.spec.message.Seg;
063 import ca.uhn.hl7v2.conf.spec.message.SegGroup;
064 import ca.uhn.hl7v2.conf.spec.message.StaticDef;
065 import ca.uhn.hl7v2.conf.spec.message.SubComponent;
066
067 /**
068 * <p>
069 * Parses a Message Profile XML document into a RuntimeProfile object. A Message
070 * Profile is a formal description of additional constraints on a message
071 * (beyond what is specified in the HL7 specification), usually for a particular
072 * system, region, etc. Message profiles are introduced in HL7 version 2.5
073 * section 2.12. The RuntimeProfile object is simply an object representation of
074 * the profile, which may be used for validating messages or editing the
075 * profile.
076 * </p>
077 * <p>
078 * Example usage: <code><pre>
079 * // Load the profile from the classpath
080 * ProfileParser parser = new ProfileParser(false);
081 * RuntimeProfile profile = parser.parseClasspath("ca/uhn/hl7v2/conf/parser/example_ack.xml");
082 *
083 * // Create a message to validate
084 * String message = "MSH|^~\\&|||||||ACK^A01|1|D|2.4|||||CAN|wrong|F^^HL70001^x^^HL78888|\r"; //note HL7888 doesn't exist
085 * ACK msg = (ACK) (new PipeParser()).parse(message);
086 *
087 * // Validate
088 * HL7Exception[] errors = new DefaultValidator().validate(msg, profile.getMessage());
089 *
090 * // Each exception is a validation error
091 * System.out.println("Validation errors: " + Arrays.asList(errors));
092 * </pre></code>
093 * </p>
094 *
095 * @author Bryan Tripp
096 */
097 public class ProfileParser {
098
099 private static final Logger log = LoggerFactory.getLogger(ProfileParser.class);
100
101 private DOMParser parser;
102 private boolean alwaysValidate;
103
104 /**
105 * Creates a new instance of ProfileParser
106 *
107 * @param alwaysValidateAgainstDTD
108 * if true, validates all profiles against a local copy of the
109 * profile DTD; if false, validates against declared grammar (if
110 * any)
111 */
112 public ProfileParser(boolean alwaysValidateAgainstDTD) {
113
114 this.alwaysValidate = alwaysValidateAgainstDTD;
115
116 parser = new DOMParser(new StandardParserConfiguration());
117 try {
118 parser.setFeature("http://apache.org/xml/features/dom/include-ignorable-whitespace", false);
119 } catch (Exception e) {
120 log.error("Can't exclude whitespace from XML DOM", e);
121 }
122 try {
123 parser.setFeature("http://apache.org/xml/features/validation/dynamic", true);
124 } catch (Exception e) {
125 log.error("Can't validate profile against XML grammar", e);
126 }
127 parser.setErrorHandler(new ErrorHandler() {
128 public void error(SAXParseException e) throws SAXException {
129 throw e;
130 }
131
132 public void fatalError(SAXParseException e) throws SAXException {
133 throw e;
134 }
135
136 public void warning(SAXParseException e) throws SAXException {
137 System.err.println("Warning: " + e.getMessage());
138 }
139
140 });
141
142 if (alwaysValidateAgainstDTD) {
143 try {
144 final String grammar = loadGrammar();
145 parser.setEntityResolver(new EntityResolver() {
146 // returns the grammar we specify no matter what the
147 // document declares
148 public InputSource resolveEntity(String publicID, String systemID) throws SAXException, IOException {
149 return new InputSource(new StringReader(grammar));
150 }
151 });
152 } catch (IOException e) {
153 log.error("Can't validate profiles against XML grammar", e);
154 }
155 }
156
157 }
158
159 /** Loads the XML grammar from disk */
160 private String loadGrammar() throws IOException {
161 InputStream dtdStream = ProfileParser.class.getClassLoader().getResourceAsStream("ca/uhn/hl7v2/conf/parser/message_profile.dtd");
162 BufferedReader dtdReader = new BufferedReader(new InputStreamReader(dtdStream));
163 String line = null;
164 StringBuffer dtd = new StringBuffer();
165 while ((line = dtdReader.readLine()) != null) {
166 dtd.append(line);
167 dtd.append("\r\n");
168 }
169 return dtd.toString();
170 }
171
172 /**
173 * Parses an XML profile string into a RuntimeProfile object.
174 *
175 * Input is a path pointing to a textual file on the classpath. Note that
176 * the file will be read using the thread context class loader.
177 *
178 * For example, if you had a file called PROFILE.TXT in package
179 * com.foo.stuff, you would pass in "com/foo/stuff/PROFILE.TXT"
180 *
181 * @throws IOException
182 * If the resource can't be read
183 */
184 public RuntimeProfile parseClasspath(String classPath) throws ProfileException, IOException {
185
186 InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPath);
187 if (stream == null) {
188 throw new FileNotFoundException(classPath);
189 }
190
191 StringBuffer profileString = new StringBuffer();
192 byte[] buffer = new byte[1000];
193 int bytesRead;
194 while ((bytesRead = stream.read(buffer)) > 0) {
195 profileString.append(new String(buffer, 0, bytesRead));
196 }
197
198 RuntimeProfile profile = new RuntimeProfile();
199 Document doc = parseIntoDOM(profileString.toString());
200
201 Element root = doc.getDocumentElement();
202 profile.setHL7Version(root.getAttribute("HL7Version"));
203
204 // get static definition
205 NodeList nl = root.getElementsByTagName("HL7v2xStaticDef");
206 Element staticDef = (Element) nl.item(0);
207 StaticDef sd = parseStaticProfile(staticDef);
208 profile.setMessage(sd);
209 return profile;
210 }
211
212 /**
213 * Parses an XML profile string into a RuntimeProfile object.
214 */
215 public RuntimeProfile parse(String profileString) throws ProfileException {
216 RuntimeProfile profile = new RuntimeProfile();
217 Document doc = parseIntoDOM(profileString);
218
219 Element root = doc.getDocumentElement();
220 profile.setHL7Version(root.getAttribute("HL7Version"));
221
222 NodeList metadataList = root.getElementsByTagName("MetaData");
223 if (metadataList.getLength() > 0) {
224 Element metadata = (Element) metadataList.item(0);
225 String name = metadata.getAttribute("Name");
226 profile.setName(name);
227 }
228
229 // get static definition
230 NodeList nl = root.getElementsByTagName("HL7v2xStaticDef");
231 Element staticDef = (Element) nl.item(0);
232 StaticDef sd = parseStaticProfile(staticDef);
233 profile.setMessage(sd);
234 return profile;
235 }
236
237 private StaticDef parseStaticProfile(Element elem) throws ProfileException {
238 StaticDef message = new StaticDef();
239 message.setMsgType(elem.getAttribute("MsgType"));
240 message.setEventType(elem.getAttribute("EventType"));
241 message.setMsgStructID(elem.getAttribute("MsgStructID"));
242 message.setOrderControl(elem.getAttribute("OrderControl"));
243 message.setEventDesc(elem.getAttribute("EventDesc"));
244 message.setIdentifier(elem.getAttribute("identifier"));
245 message.setRole(elem.getAttribute("role"));
246
247 Element md = getFirstElementByTagName("MetaData", elem);
248 if (md != null)
249 message.setMetaData(parseMetaData(md));
250
251 message.setImpNote(getValueOfFirstElement("ImpNote", elem));
252 message.setDescription(getValueOfFirstElement("Description", elem));
253 message.setReference(getValueOfFirstElement("Reference", elem));
254
255 parseChildren(message, elem);
256 return message;
257 }
258
259 /** Parses metadata element */
260 private MetaData parseMetaData(Element elem) {
261 log.debug("ProfileParser.parseMetaData() has been called ... note that this method does nothing.");
262 return null;
263 }
264
265 /**
266 * Parses children (i.e. segment groups, segments) of a segment group or
267 * message profile
268 */
269 private void parseChildren(AbstractSegmentContainer parent, Element elem) throws ProfileException {
270 int childIndex = 1;
271 NodeList children = elem.getChildNodes();
272 for (int i = 0; i < children.getLength(); i++) {
273 Node n = children.item(i);
274 if (n.getNodeType() == Node.ELEMENT_NODE) {
275 Element child = (Element) n;
276 if (child.getNodeName().equalsIgnoreCase("SegGroup")) {
277 SegGroup group = parseSegmentGroupProfile(child);
278 parent.setChild(childIndex++, group);
279 } else if (child.getNodeName().equalsIgnoreCase("Segment")) {
280 Seg segment = parseSegmentProfile(child);
281 parent.setChild(childIndex++, segment);
282 }
283 }
284 }
285 }
286
287 /** Parses a segment group profile */
288 private SegGroup parseSegmentGroupProfile(Element elem) throws ProfileException {
289 SegGroup group = new SegGroup();
290 log.debug("Parsing segment group profile: " + elem.getAttribute("Name"));
291
292 parseProfileStuctureData(group, elem);
293
294 parseChildren(group, elem);
295 return group;
296 }
297
298 /** Parses a segment profile */
299 private Seg parseSegmentProfile(Element elem) throws ProfileException {
300 Seg segment = new Seg();
301 log.debug("Parsing segment profile: " + elem.getAttribute("Name"));
302
303 parseProfileStuctureData(segment, elem);
304
305 int childIndex = 1;
306 NodeList children = elem.getChildNodes();
307 for (int i = 0; i < children.getLength(); i++) {
308 Node n = children.item(i);
309 if (n.getNodeType() == Node.ELEMENT_NODE) {
310 Element child = (Element) n;
311 if (child.getNodeName().equalsIgnoreCase("Field")) {
312 Field field = parseFieldProfile(child);
313 segment.setField(childIndex++, field);
314 }
315 }
316 }
317
318 return segment;
319 }
320
321 /** Parse common data in profile structure (eg SegGroup, Segment) */
322 private void parseProfileStuctureData(ProfileStructure struct, Element elem) throws ProfileException {
323 struct.setName(elem.getAttribute("Name"));
324 struct.setLongName(elem.getAttribute("LongName"));
325 struct.setUsage(elem.getAttribute("Usage"));
326 String min = elem.getAttribute("Min");
327 String max = elem.getAttribute("Max");
328 try {
329 struct.setMin(Short.parseShort(min));
330 if (max.indexOf('*') >= 0) {
331 struct.setMax((short) -1);
332 } else {
333 struct.setMax(Short.parseShort(max));
334 }
335 } catch (NumberFormatException e) {
336 throw new ProfileException("Min and max must be short integers: " + min + ", " + max, e);
337 }
338
339 struct.setImpNote(getValueOfFirstElement("ImpNote", elem));
340 struct.setDescription(getValueOfFirstElement("Description", elem));
341 struct.setReference(getValueOfFirstElement("Reference", elem));
342 struct.setPredicate(getValueOfFirstElement("Predicate", elem));
343 }
344
345 /** Parses a field profile */
346 private Field parseFieldProfile(Element elem) throws ProfileException {
347 Field field = new Field();
348 log.debug(" Parsing field profile: " + elem.getAttribute("Name"));
349
350 field.setUsage(elem.getAttribute("Usage"));
351 String itemNo = elem.getAttribute("ItemNo");
352 String min = elem.getAttribute("Min");
353 String max = elem.getAttribute("Max");
354
355 try {
356 if (itemNo.length() > 0) {
357 field.setItemNo(Short.parseShort(itemNo));
358 }
359 } catch (NumberFormatException e) {
360 throw new ProfileException("Invalid ItemNo: " + itemNo + "( for name " + elem.getAttribute("Name") + ")", e);
361 } // try-catch
362
363 try {
364 field.setMin(Short.parseShort(min));
365 if (max.indexOf('*') >= 0) {
366 field.setMax((short) -1);
367 } else {
368 field.setMax(Short.parseShort(max));
369 }
370 } catch (NumberFormatException e) {
371 throw new ProfileException("Min and max must be short integers: " + min + ", " + max, e);
372 }
373
374 parseAbstractComponentData(field, elem);
375
376 int childIndex = 1;
377 NodeList children = elem.getChildNodes();
378 for (int i = 0; i < children.getLength(); i++) {
379 Node n = children.item(i);
380 if (n.getNodeType() == Node.ELEMENT_NODE) {
381 Element child = (Element) n;
382 if (child.getNodeName().equalsIgnoreCase("Component")) {
383 Component comp = (Component) parseComponentProfile(child, false);
384 field.setComponent(childIndex++, comp);
385 }
386 }
387 }
388
389 return field;
390 }
391
392 /** Parses a component profile */
393 private AbstractComponent parseComponentProfile(Element elem, boolean isSubComponent) throws ProfileException {
394 AbstractComponent comp = null;
395 if (isSubComponent) {
396 log.debug(" Parsing subcomp profile: " + elem.getAttribute("Name"));
397 comp = new SubComponent();
398 } else {
399 log.debug(" Parsing comp profile: " + elem.getAttribute("Name"));
400 comp = new Component();
401
402 int childIndex = 1;
403 NodeList children = elem.getChildNodes();
404 for (int i = 0; i < children.getLength(); i++) {
405 Node n = children.item(i);
406 if (n.getNodeType() == Node.ELEMENT_NODE) {
407 Element child = (Element) n;
408 if (child.getNodeName().equalsIgnoreCase("SubComponent")) {
409 SubComponent subcomp = (SubComponent) parseComponentProfile(child, true);
410 ((Component) comp).setSubComponent(childIndex++, subcomp);
411 }
412 }
413 }
414 }
415
416 parseAbstractComponentData(comp, elem);
417
418 return comp;
419 }
420
421 /**
422 * Parses common features of AbstractComponents (ie field, component,
423 * subcomponent)
424 */
425 private void parseAbstractComponentData(AbstractComponent comp, Element elem) throws ProfileException {
426 comp.setName(elem.getAttribute("Name"));
427 comp.setUsage(elem.getAttribute("Usage"));
428 comp.setDatatype(elem.getAttribute("Datatype"));
429 String length = elem.getAttribute("Length");
430 if (length != null && length.length() > 0) {
431 try {
432 comp.setLength(Long.parseLong(length));
433 } catch (NumberFormatException e) {
434 throw new ProfileException("Length must be a long integer: " + length, e);
435 }
436 }
437 comp.setConstantValue(elem.getAttribute("ConstantValue"));
438 String table = elem.getAttribute("Table");
439 if (table != null && table.length() > 0) {
440 try {
441 comp.setTable(table);
442 } catch (NumberFormatException e) {
443 throw new ProfileException("Table must be a short integer: " + table, e);
444 }
445 }
446
447 comp.setImpNote(getValueOfFirstElement("ImpNote", elem));
448 comp.setDescription(getValueOfFirstElement("Description", elem));
449 comp.setReference(getValueOfFirstElement("Reference", elem));
450 comp.setPredicate(getValueOfFirstElement("Predicate", elem));
451
452 int dataValIndex = 0;
453 NodeList children = elem.getChildNodes();
454 for (int i = 0; i < children.getLength(); i++) {
455 Node n = children.item(i);
456 if (n.getNodeType() == Node.ELEMENT_NODE) {
457 Element child = (Element) n;
458 if (child.getNodeName().equalsIgnoreCase("DataValues")) {
459 DataValue val = new DataValue();
460 val.setExValue(child.getAttribute("ExValue"));
461 comp.setDataValues(dataValIndex++, val);
462 }
463 }
464 }
465
466 }
467
468 /** Parses profile string into DOM document */
469 private Document parseIntoDOM(String profileString) throws ProfileException {
470 if (this.alwaysValidate)
471 profileString = insertDoctype(profileString);
472 Document doc = null;
473 try {
474 synchronized (this) {
475 parser.parse(new InputSource(new StringReader(profileString)));
476 log.debug("DOM parse complete");
477 doc = parser.getDocument();
478 }
479 } catch (SAXException se) {
480 throw new ProfileException("SAXException parsing message profile: " + se.getMessage());
481 } catch (IOException ioe) {
482 throw new ProfileException("IOException parsing message profile: " + ioe.getMessage());
483 }
484 return doc;
485 }
486
487 /** Inserts a DOCTYPE declaration in the string if there isn't one */
488 private String insertDoctype(String profileString) {
489 String result = profileString;
490 if (profileString.indexOf("<!DOCTYPE") < 0) {
491 StringBuffer buf = new StringBuffer();
492 int loc = profileString.indexOf("?>");
493 if (loc > 0) {
494 buf.append(profileString.substring(0, loc + 2));
495 buf.append("<!DOCTYPE HL7v2xConformanceProfile SYSTEM \"\">");
496 buf.append(profileString.substring(loc + 2));
497 result = buf.toString();
498 }
499 }
500 return result;
501 }
502
503 /**
504 * Returns the first child element of the given parent that matches the
505 * given tag name. Returns null if no instance of the expected element is
506 * present.
507 */
508 private Element getFirstElementByTagName(String name, Element parent) {
509 NodeList nl = parent.getElementsByTagName(name);
510 Element ret = null;
511 if (nl.getLength() > 0) {
512 ret = (Element) nl.item(0);
513 }
514 return ret;
515 }
516
517 /**
518 * Gets the result of getFirstElementByTagName() and returns the value of
519 * that element.
520 */
521 private String getValueOfFirstElement(String name, Element parent) throws ProfileException {
522 Element el = getFirstElementByTagName(name, parent);
523 String val = null;
524 if (el != null) {
525 try {
526 Node n = el.getFirstChild();
527 if (n.getNodeType() == Node.TEXT_NODE) {
528 val = n.getNodeValue();
529 }
530 } catch (Exception e) {
531 throw new ProfileException("Unable to get value of node " + name, e);
532 }
533 }
534 return val;
535 }
536
537 public static void main(String args[]) {
538
539 if (args.length != 1) {
540 System.out.println("Usage: ProfileParser profile_file");
541 System.exit(1);
542 }
543
544 try {
545 // File f = new
546 // File("C:\\Documents and Settings\\bryan\\hapilocal\\hapi\\ca\\uhn\\hl7v2\\conf\\parser\\example_ack.xml");
547 File f = new File(args[0]);
548 BufferedReader in = new BufferedReader(new FileReader(f));
549 char[] cbuf = new char[(int) f.length()];
550 in.read(cbuf, 0, (int) f.length());
551 String xml = String.valueOf(cbuf);
552 // System.out.println(xml);
553
554 ProfileParser pp = new ProfileParser(true);
555 pp.parse(xml);
556 } catch (Exception e) {
557 e.printStackTrace();
558 }
559 }
560
561 }