001/** 002The 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. 004You may obtain a copy of the License at http://www.mozilla.org/MPL/ 005Software distributed under the License is distributed on an "AS IS" basis, 006WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 007specific language governing rights and limitations under the License. 008 009The Original Code is "ProfileParser.java". Description: 010"Parses a Message Profile XML document into a RuntimeProfile object." 011 012The Initial Developer of the Original Code is University Health Network. Copyright (C) 0132003. All Rights Reserved. 014 015Contributor(s): ______________________________________. 016 017Alternatively, the contents of this file may be used under the terms of the 018GNU General Public License (the "GPL"), in which case the provisions of the GPL are 019applicable instead of those above. If you wish to allow use of your version of this 020file only under the terms of the GPL and not to allow others to use your version 021of this file under the MPL, indicate your decision by deleting the provisions above 022and replace them with the notice and other provisions required by the GPL License. 023If you do not delete the provisions above, a recipient may use your version of 024this file under either the MPL or the GPL. 025 026 */ 027 028package ca.uhn.hl7v2.conf.parser; 029 030import java.io.BufferedReader; 031import java.io.File; 032import java.io.FileNotFoundException; 033import java.io.FileReader; 034import java.io.IOException; 035import java.io.InputStream; 036import java.io.InputStreamReader; 037import java.io.StringReader; 038 039import org.apache.xerces.parsers.DOMParser; 040import org.apache.xerces.parsers.StandardParserConfiguration; 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043import org.w3c.dom.Document; 044import org.w3c.dom.Element; 045import org.w3c.dom.Node; 046import org.w3c.dom.NodeList; 047import org.xml.sax.EntityResolver; 048import org.xml.sax.ErrorHandler; 049import org.xml.sax.InputSource; 050import org.xml.sax.SAXException; 051import org.xml.sax.SAXParseException; 052 053import ca.uhn.hl7v2.conf.ProfileException; 054import ca.uhn.hl7v2.conf.spec.MetaData; 055import ca.uhn.hl7v2.conf.spec.RuntimeProfile; 056import ca.uhn.hl7v2.conf.spec.message.AbstractComponent; 057import ca.uhn.hl7v2.conf.spec.message.AbstractSegmentContainer; 058import ca.uhn.hl7v2.conf.spec.message.Component; 059import ca.uhn.hl7v2.conf.spec.message.DataValue; 060import ca.uhn.hl7v2.conf.spec.message.Field; 061import ca.uhn.hl7v2.conf.spec.message.ProfileStructure; 062import ca.uhn.hl7v2.conf.spec.message.Seg; 063import ca.uhn.hl7v2.conf.spec.message.SegGroup; 064import ca.uhn.hl7v2.conf.spec.message.StaticDef; 065import 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 */ 097public 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}