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