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