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 "XMLParser.java". Description: 010 * "Parses and encodes HL7 messages in XML form, according to HL7's normative XML encoding 011 * specification." 012 * 013 * The Initial Developer of the Original Code is University Health Network. Copyright (C) 014 * 2002. All Rights Reserved. 015 * 016 * Contributor(s): ______________________________________. 017 * 018 * Alternatively, the contents of this file may be used under the terms of the 019 * GNU General Public License (the �GPL�), in which case the provisions of the GPL are 020 * applicable instead of those above. If you wish to allow use of your version of this 021 * file only under the terms of the GPL and not to allow others to use your version 022 * of this file under the MPL, indicate your decision by deleting the provisions above 023 * and replace them with the notice and other provisions required by the GPL License. 024 * If you do not delete the provisions above, a recipient may use your version of 025 * this file under either the MPL or the GPL. 026 */ 027 028package ca.uhn.hl7v2.parser; 029 030import java.io.File; 031import java.io.FileReader; 032import java.io.IOException; 033import java.io.StringReader; 034import java.io.StringWriter; 035import java.util.HashSet; 036import java.util.Set; 037 038import javax.xml.parsers.DocumentBuilder; 039import javax.xml.parsers.DocumentBuilderFactory; 040 041import org.apache.xerces.parsers.DOMParser; 042import org.apache.xerces.parsers.StandardParserConfiguration; 043import org.apache.xml.serialize.OutputFormat; 044import org.apache.xml.serialize.XMLSerializer; 045import org.slf4j.Logger; 046import org.slf4j.LoggerFactory; 047import org.w3c.dom.DOMException; 048import org.w3c.dom.Document; 049import org.w3c.dom.Element; 050import org.w3c.dom.Node; 051import org.w3c.dom.NodeList; 052import org.w3c.dom.Text; 053import org.xml.sax.InputSource; 054import org.xml.sax.SAXException; 055 056import ca.uhn.hl7v2.HL7Exception; 057import ca.uhn.hl7v2.model.Composite; 058import ca.uhn.hl7v2.model.DataTypeException; 059import ca.uhn.hl7v2.model.GenericComposite; 060import ca.uhn.hl7v2.model.GenericMessage; 061import ca.uhn.hl7v2.model.GenericPrimitive; 062import ca.uhn.hl7v2.model.Message; 063import ca.uhn.hl7v2.model.Primitive; 064import ca.uhn.hl7v2.model.Segment; 065import ca.uhn.hl7v2.model.Structure; 066import ca.uhn.hl7v2.model.Type; 067import ca.uhn.hl7v2.model.Varies; 068import ca.uhn.hl7v2.util.Terser; 069 070/** 071 * Parses and encodes HL7 messages in XML form, according to HL7's normative XML encoding 072 * specification. This is an abstract class that handles datatype and segment parsing/encoding, 073 * but not the parsing/encoding of entire messages. To use the XML parser, you should create a 074 * subclass for a certain message structure. This subclass must be able to identify the Segment 075 * objects that correspond to various Segment nodes in an XML document, and call the methods <code> 076 * parse(Segment segment, ElementNode segmentNode)</code> and <code>encode(Segment segment, ElementNode segmentNode) 077 * </code> as appropriate. XMLParser uses the Xerces parser, which must be installed in your classpath. 078 * @author Bryan Tripp, Shawn Bellina 079 */ 080public abstract class XMLParser extends Parser { 081 082 private static final Logger log = LoggerFactory.getLogger(XMLParser.class); 083 084 private DOMParser parser; 085 private String textEncoding; 086 087 /** 088 * The nodes whose names match these strings will be kept as original, 089 * meaning that no white space treaming will occur on them 090 */ 091 private String[] keepAsOriginalNodes; 092 093 /** 094 * All keepAsOriginalNodes names, concatenated by a pipe (|) 095 */ 096 private String concatKeepAsOriginalNodes = ""; 097 098 /** Constructor */ 099 public XMLParser() { 100 this(null); 101 } 102 103 /** 104 * Constructor 105 * 106 * @param theFactory custom factory to use for model class lookup 107 */ 108 public XMLParser(ModelClassFactory theFactory) { 109 super(theFactory); 110 parser = new DOMParser(new StandardParserConfiguration()); 111 try { 112 parser.setFeature("http://apache.org/xml/features/dom/include-ignorable-whitespace", false); 113 } 114 catch (Exception e) { 115 log.error("Can't exclude whitespace from XML DOM", e); 116 } 117 } 118 119 /** 120 * Returns a String representing the encoding of the given message, if 121 * the encoding is recognized. For example if the given message appears 122 * to be encoded using HL7 2.x XML rules then "XML" would be returned. 123 * If the encoding is not recognized then null is returned. That this 124 * method returns a specific encoding does not guarantee that the 125 * message is correctly encoded (e.g. well formed XML) - just that 126 * it is not encoded using any other encoding than the one returned. 127 * Returns null if the encoding is not recognized. 128 */ 129 public String getEncoding(String message) { 130 if (EncodingDetector.isXmlEncoded(message)) { 131 return "XML"; 132 } 133 return null; 134 } 135 136 137 /** 138 * Returns true if and only if the given encoding is supported 139 * by this Parser. 140 */ 141 public boolean supportsEncoding(String encoding) { 142 if (encoding.equals("XML")) { 143 return true; 144 } 145 else { 146 return false; 147 } 148 } 149 150 /** 151 * @return the preferred encoding of this Parser 152 */ 153 public String getDefaultEncoding() { 154 return "XML"; 155 } 156 157 /** 158 * Sets the <i>keepAsOriginalNodes<i> 159 * 160 * The nodes whose names match the <i>keepAsOriginalNodes<i> will be kept as original, 161 * meaning that no white space treaming will occur on them 162 */ 163 public void setKeepAsOriginalNodes(String[] keepAsOriginalNodes) { 164 this.keepAsOriginalNodes = keepAsOriginalNodes; 165 166 if (keepAsOriginalNodes.length != 0) { 167 //initializes the 168 StringBuffer strBuf = new StringBuffer(keepAsOriginalNodes[0]); 169 for (int i = 1; i < keepAsOriginalNodes.length; i++) { 170 strBuf.append("|"); 171 strBuf.append(keepAsOriginalNodes[i]); 172 } 173 concatKeepAsOriginalNodes = strBuf.toString(); 174 } 175 else { 176 concatKeepAsOriginalNodes = ""; 177 } 178 } 179 180 /** 181 * Sets the <i>keepAsOriginalNodes<i> 182 */ 183 public String[] getKeepAsOriginalNodes() { 184 return keepAsOriginalNodes; 185 } 186 187 /** 188 * <p>Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 message.</p> 189 * <p>The easiest way to implement this method for a particular message structure is as follows: 190 * <ol><li>Create an instance of the Message type you are going to handle with your subclass 191 * of XMLParser</li> 192 * <li>Go through the given Document and find the Elements that represent the top level of 193 * each message segment. </li> 194 * <li>For each of these segments, call <code>parse(Segment segmentObject, Element segmentElement)</code>, 195 * providing the appropriate Segment from your Message object, and the corresponding Element.</li></ol> 196 * At the end of this process, your Message object should be populated with data from the XML 197 * Document.</p> 198 * @throws HL7Exception if the message is not correctly formatted. 199 * @throws EncodingNotSupportedException if the message encoded 200 * is not supported by this parser. 201 */ 202 public abstract Message parseDocument(Document XMLMessage, String version) throws HL7Exception; 203 204 /** 205 * <p>Parses a message string and returns the corresponding Message 206 * object. This method checks that the given message string is XML encoded, creates an 207 * XML Document object (using Xerces) from the given String, and calls the abstract 208 * method <code>parse(Document XMLMessage)</code></p> 209 */ 210 protected Message doParse(String message, String version) throws HL7Exception, EncodingNotSupportedException { 211 Message m = null; 212 213 //parse message string into a DOM document 214 Document doc = null; 215 doc = parseStringIntoDocument(message); 216 m = parseDocument(doc, version); 217 218 return m; 219 } 220 221 /** 222 * Parses a string containing an XML document into a Document object. 223 * 224 * Note that this method is synchronized currently, as the XML parser is 225 * not thread safe 226 * @throws HL7Exception 227 */ 228 protected synchronized Document parseStringIntoDocument(String message) throws HL7Exception { 229 Document doc; 230 try { 231 parser.parse(new InputSource(new StringReader(message))); 232 } 233 catch (SAXException e) { 234 throw new HL7Exception("SAXException parsing XML", HL7Exception.APPLICATION_INTERNAL_ERROR, e); 235 } 236 catch (IOException e) { 237 throw new HL7Exception("IOException parsing XML", HL7Exception.APPLICATION_INTERNAL_ERROR, e); 238 } 239 240 doc = parser.getDocument(); 241 return doc; 242 } 243 244 /** 245 * Formats a Message object into an HL7 message string using the given 246 * encoding. 247 * @throws HL7Exception if the data fields in the message do not permit encoding 248 * (e.g. required fields are null) 249 * @throws EncodingNotSupportedException if the requested encoding is not 250 * supported by this parser. 251 */ 252 protected String doEncode(Message source, String encoding) throws HL7Exception, EncodingNotSupportedException { 253 if (!encoding.equals("XML")) 254 throw new EncodingNotSupportedException("XMLParser supports only XML encoding"); 255 return encode(source); 256 } 257 258 /** 259 * Formats a Message object into an HL7 message string using this parser's 260 * default encoding (XML encoding). This method calls the abstract method 261 * <code>encodeDocument(...)</code> in order to obtain XML Document object 262 * representation of the Message, then serializes it to a String. 263 * @throws HL7Exception if the data fields in the message do not permit encoding 264 * (e.g. required fields are null) 265 */ 266 protected String doEncode(Message source) throws HL7Exception { 267 if (source instanceof GenericMessage) { 268 throw new HL7Exception("Can't XML-encode a GenericMessage. Message must have a recognized structure."); 269 } 270 271 Document doc = encodeDocument(source); 272 Element documentElement = doc.getDocumentElement(); 273 documentElement.setAttribute("xmlns", "urn:hl7-org:v2xml"); 274 275 StringWriter out = new StringWriter(); 276 277 OutputFormat outputFormat = new OutputFormat("", null, true); 278 outputFormat.setLineWidth(0); 279 280 if (textEncoding != null) { 281 outputFormat.setEncoding(textEncoding); 282 } 283 284 XMLSerializer ser = new XMLSerializer(out, outputFormat); //default output format 285 try { 286 ser.serialize(doc); 287 } 288 catch (IOException e) { 289 throw new HL7Exception( 290 "IOException serializing XML document to string", 291 HL7Exception.APPLICATION_INTERNAL_ERROR, 292 e); 293 } 294 return out.toString(); 295 } 296 297 /** 298 * <p>Creates an XML Document that corresponds to the given Message object. </p> 299 * <p>If you are implementing this method, you should create an XML Document, and insert XML Elements 300 * into it that correspond to the groups and segments that belong to the message type that your subclass 301 * of XMLParser supports. Then, for each segment in the message, call the method 302 * <code>encode(Segment segmentObject, Element segmentElement)</code> using the Element for 303 * that segment and the corresponding Segment object from the given Message.</p> 304 */ 305 public abstract Document encodeDocument(Message source) throws HL7Exception; 306 307 /** 308 * Populates the given Segment object with data from the given XML Element. 309 * @throws HL7Exception if the XML Element does not have the correct name and structure 310 * for the given Segment, or if there is an error while setting individual field values. 311 */ 312 public void parse(Segment segmentObject, Element segmentElement) throws HL7Exception { 313 Set<String> done = new HashSet<String>(); 314 315// for (int i = 1; i <= segmentObject.numFields(); i++) { 316// String elementName = makeElementName(segmentObject, i); 317// done.add(elementName); 318// parseReps(segmentObject, segmentElement, elementName, i); 319// } 320 321 NodeList all = segmentElement.getChildNodes(); 322 for (int i = 0; i < all.getLength(); i++) { 323 String elementName = all.item(i).getNodeName(); 324 if (all.item(i).getNodeType() == Node.ELEMENT_NODE && !done.contains(elementName)) { 325 done.add(elementName); 326 327 int index = elementName.indexOf('.'); 328 if (index >= 0 && elementName.length() > index) { //properly formatted element 329 String fieldNumString = elementName.substring(index + 1); 330 int fieldNum = Integer.parseInt(fieldNumString); 331 parseReps(segmentObject, segmentElement, elementName, fieldNum); 332 } else { 333 log.debug("Child of segment {} doesn't look like a field {}", 334 segmentObject.getName(), elementName); 335 } 336 } 337 } 338 339 //set data type of OBX-5 340 if (segmentObject.getClass().getName().indexOf("OBX") >= 0) { 341 Varies.fixOBX5(segmentObject, getFactory()); 342 } 343 } 344 345 private void parseReps(Segment segmentObject, Element segmentElement, String fieldName, int fieldNum) 346 throws DataTypeException, HL7Exception { 347 348 NodeList reps = segmentElement.getElementsByTagName(fieldName); 349 for (int i = 0; i < reps.getLength(); i++) { 350 parse(segmentObject.getField(fieldNum, i), (Element) reps.item(i)); 351 } 352 } 353 354 /** 355 * Populates the given Element with data from the given Segment, by inserting 356 * Elements corresponding to the Segment's fields, their components, etc. Returns 357 * true if there is at least one data value in the segment. 358 */ 359 public boolean encode(Segment segmentObject, Element segmentElement) throws HL7Exception { 360 boolean hasValue = false; 361 int n = segmentObject.numFields(); 362 for (int i = 1; i <= n; i++) { 363 String name = makeElementName(segmentObject, i); 364 Type[] reps = segmentObject.getField(i); 365 for (int j = 0; j < reps.length; j++) { 366 Element newNode = segmentElement.getOwnerDocument().createElement(name); 367 boolean componentHasValue = encode(reps[j], newNode); 368 if (componentHasValue) { 369 try { 370 segmentElement.appendChild(newNode); 371 } 372 catch (DOMException e) { 373 throw new HL7Exception( 374 "DOMException encoding Segment: ", 375 HL7Exception.APPLICATION_INTERNAL_ERROR, 376 e); 377 } 378 hasValue = true; 379 } 380 } 381 } 382 return hasValue; 383 } 384 385 /** 386 * Populates the given Type object with data from the given XML Element. 387 */ 388 public void parse(Type datatypeObject, Element datatypeElement) throws DataTypeException { 389 if (datatypeObject instanceof Varies) { 390 parseVaries((Varies) datatypeObject, datatypeElement); 391 } 392 else if (datatypeObject instanceof Primitive) { 393 parsePrimitive((Primitive) datatypeObject, datatypeElement); 394 } 395 else if (datatypeObject instanceof Composite) { 396 parseComposite((Composite) datatypeObject, datatypeElement); 397 } 398 } 399 400 /** 401 * Parses an XML element into a Varies by determining whether the element is primitive or 402 * composite, calling setData() on the Varies with a new generic primitive or composite as appropriate, 403 * and then calling parse again with the new Type object. 404 */ 405 private void parseVaries(Varies datatypeObject, Element datatypeElement) throws DataTypeException { 406 //figure out what data type it holds 407 //short nodeType = datatypeElement.getFirstChild().getNodeType(); 408 if (!hasChildElement(datatypeElement)) { 409 //it's a primitive 410 datatypeObject.setData(new GenericPrimitive(datatypeObject.getMessage())); 411 } 412 else { 413 //it's a composite ... almost know what type, except that we don't have the version here 414 datatypeObject.setData(new GenericComposite(datatypeObject.getMessage())); 415 } 416 parse(datatypeObject.getData(), datatypeElement); 417 } 418 419 /** Returns true if any of the given element's children are elements */ 420 private boolean hasChildElement(Element e) { 421 NodeList children = e.getChildNodes(); 422 boolean hasElement = false; 423 int c = 0; 424 while (c < children.getLength() && !hasElement) { 425 if (children.item(c).getNodeType() == Node.ELEMENT_NODE) { 426 hasElement = true; 427 } 428 c++; 429 } 430 return hasElement; 431 } 432 433 /** Parses a primitive type by filling it with text child, if any */ 434 private void parsePrimitive(Primitive datatypeObject, Element datatypeElement) throws DataTypeException { 435 NodeList children = datatypeElement.getChildNodes(); 436 int c = 0; 437 boolean full = false; 438 while (c < children.getLength() && !full) { 439 Node child = children.item(c++); 440 if (child.getNodeType() == Node.TEXT_NODE) { 441 try { 442 if (child.getNodeValue() != null && !child.getNodeValue().equals("")) { 443 if (keepAsOriginal(child.getParentNode())) { 444 datatypeObject.setValue(child.getNodeValue()); 445 } 446 else { 447 datatypeObject.setValue(removeWhitespace(child.getNodeValue())); 448 } 449 } 450 } 451 catch (DOMException e) { 452 log.error("Error parsing primitive value from TEXT_NODE", e); 453 } 454 full = true; 455 } 456 } 457 } 458 459 /** 460 * Checks if <code>Node</code> content should be kept as original (ie.: whitespaces won't be removed) 461 * 462 * @param node The target <code>Node</code> 463 * @return boolean <code>true</code> if whitespaces should not be removed from node content, 464 * <code>false</code> otherwise 465 */ 466 protected boolean keepAsOriginal(Node node) { 467 if (node.getNodeName() == null) 468 return false; 469 return concatKeepAsOriginalNodes.indexOf(node.getNodeName()) != -1; 470 } 471 472 /** 473 * Removes all unecessary whitespace from the given String (intended to be used with Primitive values). 474 * This includes leading and trailing whitespace, and repeated space characters. Carriage returns, 475 * line feeds, and tabs are replaced with spaces. 476 */ 477 protected String removeWhitespace(String s) { 478 s = s.replace('\r', ' '); 479 s = s.replace('\n', ' '); 480 s = s.replace('\t', ' '); 481 482 boolean repeatedSpacesExist = true; 483 while (repeatedSpacesExist) { 484 int loc = s.indexOf(" "); 485 if (loc < 0) { 486 repeatedSpacesExist = false; 487 } 488 else { 489 StringBuffer buf = new StringBuffer(); 490 buf.append(s.substring(0, loc)); 491 buf.append(" "); 492 buf.append(s.substring(loc + 2)); 493 s = buf.toString(); 494 } 495 } 496 return s.trim(); 497 } 498 499 /** 500 * Populates a Composite type by looping through it's children, finding corresponding 501 * Elements among the children of the given Element, and calling parse(Type, Element) for 502 * each. 503 */ 504 private void parseComposite(Composite datatypeObject, Element datatypeElement) throws DataTypeException { 505 if (datatypeObject instanceof GenericComposite) { //elements won't be named GenericComposite.x 506 NodeList children = datatypeElement.getChildNodes(); 507 int compNum = 0; 508 for (int i = 0; i < children.getLength(); i++) { 509 if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { 510 Element nextElement = (Element) children.item(i); 511 String localName = nextElement.getLocalName(); 512 int dotIndex = localName.indexOf("."); 513 if (dotIndex > -1) { 514 compNum = Integer.parseInt(localName.substring(dotIndex + 1)) - 1; 515 } else { 516 log.debug("Datatype element {} doesn't have a valid numbered name, usgin default index of {}", 517 datatypeElement.getLocalName(), compNum); 518 } 519 Type nextComponent = datatypeObject.getComponent(compNum); 520 parse(nextComponent, nextElement); 521 compNum++; 522 } 523 } 524 } 525 else { 526 Type[] children = datatypeObject.getComponents(); 527 for (int i = 0; i < children.length; i++) { 528 NodeList matchingElements = 529 datatypeElement.getElementsByTagName(makeElementName(datatypeObject, i + 1)); 530 if (matchingElements.getLength() > 0) { 531 parse(children[i], (Element) matchingElements.item(0)); //components don't repeat - use 1st 532 } 533 } 534 } 535 } 536 537 /** 538 * Returns the expected XML element name for the given child of a message constituent 539 * of the given class (the class should be a Composite or Segment class). 540 */ 541 /*private String makeElementName(Class c, int child) { 542 String longClassName = c.getName(); 543 String shortClassName = longClassName.substring(longClassName.lastIndexOf('.') + 1, longClassName.length()); 544 if (shortClassName.startsWith("Valid")) { 545 shortClassName = shortClassName.substring(5, shortClassName.length()); 546 } 547 return shortClassName + "." + child; 548 }*/ 549 550 /** Returns the expected XML element name for the given child of the given Segment */ 551 private String makeElementName(Segment s, int child) { 552 return s.getName() + "." + child; 553 } 554 555 /** Returns the expected XML element name for the given child of the given Composite */ 556 private String makeElementName(Composite composite, int child) { 557 return composite.getName() + "." + child; 558 } 559 560 /** 561 * Populates the given Element with data from the given Type, by inserting 562 * Elements corresponding to the Type's components and values. Returns true if 563 * the given type contains a value (i.e. for Primitives, if getValue() doesn't 564 * return null, and for Composites, if at least one underlying Primitive doesn't 565 * return null). 566 */ 567 private boolean encode(Type datatypeObject, Element datatypeElement) throws DataTypeException { 568 boolean hasData = false; 569 if (datatypeObject instanceof Varies) { 570 hasData = encodeVaries((Varies) datatypeObject, datatypeElement); 571 } 572 else if (datatypeObject instanceof Primitive) { 573 hasData = encodePrimitive((Primitive) datatypeObject, datatypeElement); 574 } 575 else if (datatypeObject instanceof Composite) { 576 hasData = encodeComposite((Composite) datatypeObject, datatypeElement); 577 } 578 return hasData; 579 } 580 581 /** 582 * Encodes a Varies type by extracting it's data field and encoding that. Returns true 583 * if the data field (or one of its components) contains a value. 584 */ 585 private boolean encodeVaries(Varies datatypeObject, Element datatypeElement) throws DataTypeException { 586 boolean hasData = false; 587 if (datatypeObject.getData() != null) { 588 hasData = encode(datatypeObject.getData(), datatypeElement); 589 } 590 return hasData; 591 } 592 593 /** 594 * Encodes a Primitive in XML by adding it's value as a child of the given Element. 595 * Returns true if the given Primitive contains a value. 596 */ 597 private boolean encodePrimitive(Primitive datatypeObject, Element datatypeElement) throws DataTypeException { 598 boolean hasValue = false; 599 if (datatypeObject.getValue() != null && !datatypeObject.getValue().equals("")) 600 hasValue = true; 601 602 Text t = datatypeElement.getOwnerDocument().createTextNode(datatypeObject.getValue()); 603 if (hasValue) { 604 try { 605 datatypeElement.appendChild(t); 606 } 607 catch (DOMException e) { 608 throw new DataTypeException("DOMException encoding Primitive: ", e); 609 } 610 } 611 return hasValue; 612 } 613 614 /** 615 * Encodes a Composite in XML by looping through it's components, creating new 616 * children for each of them (with the appropriate names) and populating them by 617 * calling encode(Type, Element) using these children. Returns true if at least 618 * one component contains a value. 619 */ 620 private boolean encodeComposite(Composite datatypeObject, Element datatypeElement) throws DataTypeException { 621 Type[] components = datatypeObject.getComponents(); 622 boolean hasValue = false; 623 for (int i = 0; i < components.length; i++) { 624 String name = makeElementName(datatypeObject, i + 1); 625 Element newNode = datatypeElement.getOwnerDocument().createElement(name); 626 boolean componentHasValue = encode(components[i], newNode); 627 if (componentHasValue) { 628 try { 629 datatypeElement.appendChild(newNode); 630 } 631 catch (DOMException e) { 632 throw new DataTypeException("DOMException encoding Composite: ", e); 633 } 634 hasValue = true; 635 } 636 } 637 return hasValue; 638 } 639 640 /** 641 * <p>Returns a minimal amount of data from a message string, including only the 642 * data needed to send a response to the remote system. This includes the 643 * following fields: 644 * <ul><li>field separator</li> 645 * <li>encoding characters</li> 646 * <li>processing ID</li> 647 * <li>message control ID</li></ul> 648 * This method is intended for use when there is an error parsing a message, 649 * (so the Message object is unavailable) but an error message must be sent 650 * back to the remote system including some of the information in the inbound 651 * message. This method parses only that required information, hopefully 652 * avoiding the condition that caused the original error.</p> 653 */ 654 public Segment getCriticalResponseData(String message) throws HL7Exception { 655 String version = getVersion(message); 656 Segment criticalData = Parser.makeControlMSH(version, getFactory()); 657 658 Terser.set(criticalData, 1, 0, 1, 1, parseLeaf(message, "MSH.1", 0)); 659 Terser.set(criticalData, 2, 0, 1, 1, parseLeaf(message, "MSH.2", 0)); 660 Terser.set(criticalData, 10, 0, 1, 1, parseLeaf(message, "MSH.10", 0)); 661 String procID = parseLeaf(message, "MSH.11", 0); 662 if (procID == null || procID.length() == 0) { 663 procID = parseLeaf(message, "PT.1", message.indexOf("MSH.11")); 664 //this field is a composite in later versions 665 } 666 Terser.set(criticalData, 11, 0, 1, 1, procID); 667 668 return criticalData; 669 } 670 671 /** 672 * For response messages, returns the value of MSA-2 (the message ID of the message 673 * sent by the sending system). This value may be needed prior to main message parsing, 674 * so that (particularly in a multi-threaded scenario) the message can be routed to 675 * the thread that sent the request. We need this information first so that any 676 * parse exceptions are thrown to the correct thread. Implementers of Parsers should 677 * take care to make the implementation of this method very fast and robust. 678 * Returns null if MSA-2 can not be found (e.g. if the message is not a 679 * response message). Trims whitespace from around the MSA-2 field. 680 */ 681 public String getAckID(String message) { 682 String ackID = null; 683 try { 684 ackID = parseLeaf(message, "msa.2", 0).trim(); 685 } 686 catch (HL7Exception e) { /* OK ... assume it isn't a response message */ 687 } 688 return ackID; 689 } 690 691 public String getVersion(String message) throws HL7Exception { 692 String version = parseLeaf(message, "MSH.12", 0); 693 if (version == null || version.trim().length() == 0) { 694 version = parseLeaf(message, "VID.1", message.indexOf("MSH.12")); 695 } 696 return version; 697 } 698 699 /** 700 * Attempts to retrieve the value of a leaf tag without using DOM or SAX. 701 * This method searches the given message string for the given tag name, and returns 702 * everything after the given tag and before the start of the next tag. Whitespace 703 * is stripped. This is intended only for lead nodes, as the value is considered to 704 * end at the start of the next tag, regardless of whether it is the matching end 705 * tag or some other nested tag. 706 * @param message a string message in XML form 707 * @param tagName the name of the XML tag, e.g. "MSA.2" 708 * @param startAt the character location at which to start searching 709 * @throws HL7Exception if the tag can not be found 710 */ 711 protected String parseLeaf(String message, String tagName, int startAt) throws HL7Exception { 712 String value = null; 713 714 int tagStart = message.indexOf("<" + tagName, startAt); 715 if (tagStart < 0) 716 tagStart = message.indexOf("<" + tagName.toUpperCase(), startAt); 717 int valStart = message.indexOf(">", tagStart) + 1; 718 int valEnd = message.indexOf("<", valStart); 719 720 if (tagStart >= 0 && valEnd >= valStart) { 721 value = message.substring(valStart, valEnd); 722 } 723 else { 724 throw new HL7Exception( 725 "Couldn't find " 726 + tagName 727 + " in message beginning: " 728 + message.substring(0, Math.min(150, message.length())), 729 HL7Exception.REQUIRED_FIELD_MISSING); 730 } 731 732 // Escape codes, as defined at http://hdf.ncsa.uiuc.edu/HDF5/XML/xml_escape_chars.htm 733 value = value.replaceAll(""", "\""); 734 value = value.replaceAll("'", "'"); 735 value = value.replaceAll("&", "&"); 736 value = value.replaceAll("<", "<"); 737 value = value.replaceAll(">", ">"); 738 739 return value; 740 } 741 742 /** 743 * Throws unsupported operation exception 744 * 745 * @throws Unsupported operation exception 746 */ 747 @Override 748 public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception { 749 throw new UnsupportedOperationException("Not supported yet."); 750 } 751 752 /** 753 * Throws unsupported operation exception 754 * 755 * @throws Unsupported operation exception 756 */ 757 @Override 758 protected Message doParseForSpecificPackage(String theMessage, String theVersion, String thePackageName) throws HL7Exception, EncodingNotSupportedException { 759 throw new UnsupportedOperationException("Not supported yet."); 760 } 761 762 /** 763 * Throws unsupported operation exception 764 * 765 * @throws Unsupported operation exception 766 */ 767 @Override 768 public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception { 769 throw new UnsupportedOperationException("Not supported yet."); 770 } 771 772 /** 773 * Throws unsupported operation exception 774 * 775 * @throws Unsupported operation exception 776 */ 777 @Override 778 public void parse(Type type, String string, EncodingCharacters encodingCharacters) throws HL7Exception { 779 throw new UnsupportedOperationException("Not supported yet."); 780 } 781 782 /** 783 * Throws unsupported operation exception 784 * 785 * @throws Unsupported operation exception 786 */ 787 @Override 788 public void parse(Segment segment, String string, EncodingCharacters encodingCharacters) throws HL7Exception { 789 throw new UnsupportedOperationException("Not supported yet."); 790 } 791 792 793 /** Test harness */ 794 public static void main(String args[]) { 795 if (args.length != 1) { 796 System.out.println("Usage: XMLParser pipe_encoded_file"); 797 System.exit(1); 798 } 799 800 //read and parse message from file 801 try { 802 PipeParser parser = new PipeParser(); 803 File messageFile = new File(args[0]); 804 long fileLength = messageFile.length(); 805 FileReader r = new FileReader(messageFile); 806 char[] cbuf = new char[(int) fileLength]; 807 System.out.println("Reading message file ... " + r.read(cbuf) + " of " + fileLength + " chars"); 808 r.close(); 809 String messString = String.valueOf(cbuf); 810 Message mess = parser.parse(messString); 811 System.out.println("Got message of type " + mess.getClass().getName()); 812 813 ca.uhn.hl7v2.parser.XMLParser xp = new XMLParser() { 814 public Message parseDocument(Document XMLMessage, String version) throws HL7Exception { 815 return null; 816 } 817 public Document encodeDocument(Message source) throws HL7Exception { 818 return null; 819 } 820 public String getVersion(String message) throws HL7Exception { 821 return null; 822 } 823 824 @Override 825 public void parse(Message message, String string) throws HL7Exception { 826 throw new UnsupportedOperationException("Not supported yet."); 827 } 828 829 @Override 830 public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception { 831 throw new UnsupportedOperationException("Not supported yet."); 832 } 833 834 @Override 835 public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception { 836 throw new UnsupportedOperationException("Not supported yet."); 837 } 838 839 @Override 840 public void parse(Type type, String string, EncodingCharacters encodingCharacters) throws HL7Exception { 841 throw new UnsupportedOperationException("Not supported yet."); 842 } 843 844 @Override 845 public void parse(Segment segment, String string, EncodingCharacters encodingCharacters) throws HL7Exception { 846 throw new UnsupportedOperationException("Not supported yet."); 847 } 848 @Override 849 protected Message doParseForSpecificPackage(String theMessage, String theVersion, String thePackageName) throws HL7Exception, EncodingNotSupportedException { 850 throw new UnsupportedOperationException("Not supported yet."); 851 } 852 }; 853 854 //loop through segment children of message, encode, print to console 855 String[] structNames = mess.getNames(); 856 for (int i = 0; i < structNames.length; i++) { 857 Structure[] reps = mess.getAll(structNames[i]); 858 for (int j = 0; j < reps.length; j++) { 859 if (Segment.class.isAssignableFrom(reps[j].getClass())) { //ignore groups 860 DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 861 Document doc = docBuilder.newDocument(); //new doc for each segment 862 Element root = doc.createElement(reps[j].getClass().getName()); 863 doc.appendChild(root); 864 xp.encode((Segment) reps[j], root); 865 StringWriter out = new StringWriter(); 866 XMLSerializer ser = new XMLSerializer(out, null); //default output format 867 ser.serialize(doc); 868 System.out.println("Segment " + reps[j].getClass().getName() + ": \r\n" + out.toString()); 869 870 Class<?>[] segmentConstructTypes = { Message.class }; 871 Object[] segmentConstructArgs = { null }; 872 Segment s = 873 (Segment) reps[j].getClass().getConstructor(segmentConstructTypes).newInstance( 874 segmentConstructArgs); 875 xp.parse(s, root); 876 Document doc2 = docBuilder.newDocument(); 877 Element root2 = doc2.createElement(s.getClass().getName()); 878 doc2.appendChild(root2); 879 xp.encode(s, root2); 880 StringWriter out2 = new StringWriter(); 881 ser = new XMLSerializer(out2, null); //default output format 882 ser.serialize(doc2); 883 if (out2.toString().equals(out.toString())) { 884 System.out.println("Re-encode OK"); 885 } 886 else { 887 System.out.println( 888 "Warning: XML different after parse and re-encode: \r\n" + out2.toString()); 889 } 890 } 891 } 892 } 893 894 } 895 catch (Exception e) { 896 e.printStackTrace(); 897 } 898 } 899 900 /** 901 * Returns the text encoding to be used in generating new messages. Note that this affects encoding to string only, not parsing. 902 * @return 903 */ 904 public String getTextEncoding() { 905 return textEncoding; 906 } 907 908 /** 909 * Sets the text encoding to be used in generating new messages. Note that this affects encoding to string only, not parsing. 910 * @param textEncoding The encoding. Default is the platform default. 911 */ 912 public void setTextEncoding(String textEncoding) { 913 this.textEncoding = textEncoding; 914 } 915 916}