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