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 final boolean alwaysValidate; 096 private final 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 = 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}