Coverage Report - ca.uhn.hl7v2.parser.XMLParser
 
Classes in this File Line Coverage Branch Coverage Complexity
XMLParser
83%
208/249
81%
107/132
3.15
 
 1  
 /**
 2  
  * The contents of this file are subject to the Mozilla Public License Version 1.1
 3  
  * (the "License"); you may not use this file except in compliance with the License.
 4  
  * You may obtain a copy of the License at http://www.mozilla.org/MPL/
 5  
  * Software distributed under the License is distributed on an "AS IS" basis,
 6  
  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
 7  
  * specific language governing rights and limitations under the License.
 8  
  *
 9  
  * The Original Code is "XMLParser.java".  Description:
 10  
  * "Parses and encodes HL7 messages in XML form, according to HL7's normative XML encoding
 11  
  * specification."
 12  
  *
 13  
  * The Initial Developer of the Original Code is University Health Network. Copyright (C)
 14  
  * 2002.  All Rights Reserved.
 15  
  *
 16  
  * Contributor(s): ______________________________________.
 17  
  *
 18  
  * Alternatively, the contents of this file may be used under the terms of the
 19  
  * GNU General Public License (the  "GPL"), in which case the provisions of the GPL are
 20  
  * applicable instead of those above.  If you wish to allow use of your version of this
 21  
  * file only under the terms of the GPL and not to allow others to use your version
 22  
  * of this file under the MPL, indicate your decision by deleting  the provisions above
 23  
  * and replace  them with the notice and other provisions required by the GPL License.
 24  
  * If you do not delete the provisions above, a recipient may use your version of
 25  
  * this file under either the MPL or the GPL.
 26  
  */
 27  
 
 28  
 package ca.uhn.hl7v2.parser;
 29  
 
 30  
 import java.util.HashSet;
 31  
 import java.util.Set;
 32  
 
 33  
 import ca.uhn.hl7v2.ErrorCode;
 34  
 import ca.uhn.hl7v2.HL7Exception;
 35  
 import ca.uhn.hl7v2.HapiContext;
 36  
 import ca.uhn.hl7v2.model.Composite;
 37  
 import ca.uhn.hl7v2.model.DataTypeException;
 38  
 import ca.uhn.hl7v2.model.GenericComposite;
 39  
 import ca.uhn.hl7v2.model.GenericMessage;
 40  
 import ca.uhn.hl7v2.model.GenericPrimitive;
 41  
 import ca.uhn.hl7v2.model.Message;
 42  
 import ca.uhn.hl7v2.model.Primitive;
 43  
 import ca.uhn.hl7v2.model.Segment;
 44  
 import ca.uhn.hl7v2.model.Type;
 45  
 import ca.uhn.hl7v2.model.Varies;
 46  
 import ca.uhn.hl7v2.util.Terser;
 47  
 import ca.uhn.hl7v2.util.XMLUtils;
 48  
 import org.slf4j.Logger;
 49  
 import org.slf4j.LoggerFactory;
 50  
 import org.w3c.dom.DOMException;
 51  
 import org.w3c.dom.Document;
 52  
 import org.w3c.dom.Element;
 53  
 import org.w3c.dom.Node;
 54  
 import org.w3c.dom.NodeList;
 55  
 
 56  
 /**
 57  
  * Parses and encodes HL7 messages in XML form, according to HL7's normative XML encoding
 58  
  * specification. This is an abstract class that handles datatype and segment parsing/encoding, but
 59  
  * not the parsing/encoding of entire messages. To use the XML parser, you should create a subclass
 60  
  * for a certain message structure. This subclass must be able to identify the Segment objects that
 61  
  * correspond to various Segment nodes in an XML document, and call the methods <code>
 62  
  * parse(Segment segment, ElementNode segmentNode)</code> and
 63  
  * <code>encode(Segment segment, ElementNode segmentNode)
 64  
  * </code> as appropriate. XMLParser uses the Xerces parser, which must be installed in your
 65  
  * classpath.
 66  
  * 
 67  
  * @see ParserConfiguration for configuration options which may affect parser encoding and decoding behaviour
 68  
  * @author Bryan Tripp, Shawn Bellina
 69  
  */
 70  
 public abstract class XMLParser extends Parser {
 71  
 
 72  
         private static final String ESCAPE_ATTRNAME = "V";
 73  
         private static final String ESCAPE_NODENAME = "escape";
 74  1
         private static final Logger log = LoggerFactory.getLogger(XMLParser.class);
 75  
 
 76  
         private String textEncoding;
 77  
 
 78  
         /**
 79  
          * The nodes whose names match these strings will be kept as original, meaning that no white
 80  
          * space trimming will occur on them
 81  
          */
 82  
         private String[] keepAsOriginalNodes;
 83  
 
 84  
         /**
 85  
          * All keepAsOriginalNodes names, concatenated by a pipe (|)
 86  
          */
 87  75
         private String concatKeepAsOriginalNodes = "";
 88  
 
 89  
         /** Constructor */
 90  
         public XMLParser() {
 91  25
                 super();
 92  25
         }
 93  
 
 94  
     /**
 95  
      *
 96  
      * @param context the HAPI context
 97  
      */
 98  
         public XMLParser(HapiContext context) {
 99  50
                 super(context);
 100  50
         }
 101  
 
 102  
         /**
 103  
          * Constructor
 104  
          * 
 105  
          * @param theFactory custom factory to use for model class lookup
 106  
          */
 107  
         public XMLParser(ModelClassFactory theFactory) {
 108  0
                 super(theFactory);
 109  
 
 110  0
         }
 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  41
                 return EncodingDetector.isXmlEncoded(message) ? getDefaultEncoding() : null;
 122  
         }
 123  
 
 124  
         /**
 125  
          * @return the preferred encoding of this Parser
 126  
          */
 127  
         public String getDefaultEncoding() {
 128  60
                 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  0
                 this.keepAsOriginalNodes = keepAsOriginalNodes;
 141  
 
 142  0
                 if (keepAsOriginalNodes.length != 0) {
 143  
                         // initializes the
 144  0
                         StringBuilder strBuf = new StringBuilder(keepAsOriginalNodes[0]);
 145  0
                         for (int i = 1; i < keepAsOriginalNodes.length; i++) {
 146  0
                                 strBuf.append("|");
 147  0
                                 strBuf.append(keepAsOriginalNodes[i]);
 148  
                         }
 149  0
                         concatKeepAsOriginalNodes = strBuf.toString();
 150  0
                 } else {
 151  0
                         concatKeepAsOriginalNodes = "";
 152  
                 }
 153  0
         }
 154  
 
 155  
         /**
 156  
          * Sets the <i>keepAsOriginalNodes<i>
 157  
          */
 158  
         public String[] getKeepAsOriginalNodes() {
 159  0
                 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  10
                 doc = parseStringIntoDocument(message);
 202  10
                 m = parseDocument(doc, version);
 203  
 
 204  10
                 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  13
                         return XMLUtils.parse(message);
 217  0
                 } catch (Exception e) {
 218  0
                         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  0
                 if (!encoding.equals("XML"))
 232  0
                         throw new EncodingNotSupportedException("XMLParser supports only XML encoding");
 233  0
                 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  18
                 if (source instanceof GenericMessage) {
 246  0
                         throw new HL7Exception(
 247  
                                         "Can't XML-encode a GenericMessage.  Message must have a recognized structure.");
 248  
                 }
 249  
 
 250  18
                 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  18
                         return XMLUtils.serialize(doc, getParserConfiguration().isPrettyPrintWhenEncodingXml());
 256  0
                 } catch (Exception e) {
 257  0
                         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  64
                 Set<String> done = new HashSet<String>();
 288  
 
 289  64
                 NodeList all = segmentElement.getChildNodes();
 290  704
                 for (int i = 0; i < all.getLength(); i++) {
 291  640
                         String elementName = all.item(i).getNodeName();
 292  640
                         if (all.item(i).getNodeType() == Node.ELEMENT_NODE && !done.contains(elementName)) {
 293  290
                                 done.add(elementName);
 294  
 
 295  290
                                 int index = elementName.indexOf('.');
 296  290
                                 if (index >= 0 && elementName.length() > index) { // properly formatted element
 297  290
                                         String fieldNumString = elementName.substring(index + 1);
 298  290
                                         int fieldNum = Integer.parseInt(fieldNumString);
 299  290
                                         parseReps(segmentObject, segmentElement, elementName, fieldNum);
 300  290
                                 } else {
 301  0
                                         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  64
                 if (segmentObject.getClass().getName().contains("OBX")) {
 309  6
                         Varies.fixOBX5(segmentObject, getFactory(), getHapiContext().getParserConfiguration());
 310  
                 }
 311  64
         }
 312  
 
 313  
         private void parseReps(Segment segmentObject, Element segmentElement, String fieldName,
 314  
                         int fieldNum) throws HL7Exception {
 315  
 
 316  290
                 NodeList reps = segmentElement.getElementsByTagName(fieldName);
 317  584
                 for (int i = 0; i < reps.getLength(); i++) {
 318  294
                         parse(segmentObject.getField(fieldNum, i), (Element) reps.item(i));
 319  
                 }
 320  290
         }
 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  155
                 boolean hasValue = false;
 334  155
                 int n = segmentObject.numFields();
 335  3744
                 for (int i = 1; i <= n; i++) {
 336  3589
                         String name = makeElementName(segmentObject, i);
 337  3589
                         Type[] reps = segmentObject.getField(i);
 338  3832
                         for (Type rep : reps) {
 339  243
                                 Element newNode = segmentElement.getOwnerDocument().createElement(name);
 340  243
                                 boolean componentHasValue = encode(rep, newNode);
 341  243
                                 if (componentHasValue) {
 342  
                                         try {
 343  232
                                                 segmentElement.appendChild(newNode);
 344  0
                                         } catch (DOMException e) {
 345  0
                                                 throw new HL7Exception("DOMException encoding Segment: ", e);
 346  232
                                         }
 347  232
                                         hasValue = true;
 348  
                                 }
 349  
                         }
 350  
                 }
 351  155
                 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  719
                 if (datatypeObject instanceof Varies) {
 363  13
                         parseVaries((Varies) datatypeObject, datatypeElement);
 364  706
                 } else if (datatypeObject instanceof Primitive) {
 365  469
                         parsePrimitive((Primitive) datatypeObject, datatypeElement);
 366  237
                 } else if (datatypeObject instanceof Composite) {
 367  237
                         parseComposite((Composite) datatypeObject, datatypeElement);
 368  
                 }
 369  719
         }
 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  13
                 if (!hasChildElement(datatypeElement)) {
 381  
                         // it's a primitive
 382  11
                         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  2
                         datatypeObject.setData(new GenericComposite(datatypeObject.getMessage()));
 387  
                 }
 388  13
                 parse(datatypeObject.getData(), datatypeElement);
 389  13
         }
 390  
 
 391  
         /** Returns true if any of the given element's children are (non-escape) elements */
 392  
         private boolean hasChildElement(Element e) {
 393  13
                 NodeList children = e.getChildNodes();
 394  13
                 boolean hasElement = false;
 395  13
                 int c = 0;
 396  32
                 while (c < children.getLength() && !hasElement) {
 397  19
                         if (children.item(c).getNodeType() == Node.ELEMENT_NODE
 398  
                                         && !ESCAPE_NODENAME.equals(children.item(c).getNodeName())) {
 399  2
                                 hasElement = true;
 400  
                         }
 401  19
                         c++;
 402  
                 }
 403  13
                 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  469
                 NodeList children = datatypeElement.getChildNodes();
 413  469
                 StringBuilder builder = new StringBuilder();
 414  946
                 for (int c = 0; c < children.getLength(); c++) {
 415  477
                         Node child = children.item(c);
 416  
                         try {
 417  477
                                 if (child.getNodeType() == Node.TEXT_NODE) {
 418  472
                                         String value = child.getNodeValue();
 419  472
                                         if (value != null && value.length() > 0) {
 420  472
                                                 if (keepAsOriginal(child.getParentNode())) {
 421  0
                                                         builder.append(value);
 422  
                                                 } else {
 423  472
                                                         builder.append(removeWhitespace(value));
 424  
                                                 }
 425  
                                         }
 426  
                                         // Check for formatting elements
 427  472
                                 } else if (child.getNodeType() == Node.ELEMENT_NODE
 428  
                                                 && ESCAPE_NODENAME.equals(child.getNodeName())) {
 429  3
                                         EncodingCharacters ec = EncodingCharacters.getInstance(datatypeObject
 430  
                                                         .getMessage());
 431  3
                                         Element elem = (Element) child;
 432  3
                                         String attr = elem.getAttribute(ESCAPE_ATTRNAME).trim();
 433  3
                                         if (attr != null && attr.length() > 0) {
 434  3
                                                 builder.append(ec.getEscapeCharacter()).append(attr)
 435  
                                                                 .append(ec.getEscapeCharacter());
 436  
                                         }
 437  
                                 }
 438  0
                         } catch (Exception e) {
 439  0
                                 log.error("Error parsing primitive value from TEXT_NODE", e);
 440  477
                         }
 441  
 
 442  
                 }
 443  469
                 datatypeObject.setValue(builder.toString());
 444  469
         }
 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  472
                 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  474
                 s = s.replace('\r', ' ');
 465  474
                 s = s.replace('\n', ' ');
 466  474
                 s = s.replace('\t', ' ');
 467  
 
 468  474
                 boolean repeatedSpacesExist = true;
 469  1712
                 while (repeatedSpacesExist) {
 470  1238
                         int loc = s.indexOf("  ");
 471  1238
                         if (loc < 0) {
 472  474
                                 repeatedSpacesExist = false;
 473  
                         } else {
 474  764
                                 StringBuilder buf = new StringBuilder();
 475  764
                                 buf.append(s.substring(0, loc));
 476  764
                                 buf.append(" ");
 477  764
                                 buf.append(s.substring(loc + 2));
 478  764
                                 s = buf.toString();
 479  
                         }
 480  1238
                 }
 481  474
                 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  237
                 if (datatypeObject instanceof GenericComposite) { // elements won't be named
 491  
                                                                                                                         // GenericComposite.x
 492  2
                         NodeList children = datatypeElement.getChildNodes();
 493  2
                         int compNum = 0;
 494  18
                         for (int i = 0; i < children.getLength(); i++) {
 495  16
                                 if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
 496  7
                                         Element nextElement = (Element) children.item(i);
 497  7
                                         String localName = nextElement.getLocalName();
 498  7
                                         int dotIndex = localName.indexOf(".");
 499  7
                                         if (dotIndex > -1) {
 500  7
                                                 compNum = Integer.parseInt(localName.substring(dotIndex + 1)) - 1;
 501  
                                         } else {
 502  0
                                                 log.debug(
 503  
                                                                 "Datatype element {} doesn't have a valid numbered name, usgin default index of {}",
 504  
                                                                 datatypeElement.getLocalName(), compNum);
 505  
                                         }
 506  7
                                         Type nextComponent = datatypeObject.getComponent(compNum);
 507  7
                                         parse(nextComponent, nextElement);
 508  7
                                         compNum++;
 509  
                                 }
 510  
                         }
 511  2
                 } else {
 512  235
                         Type[] children = datatypeObject.getComponents();
 513  1750
                         for (int i = 0; i < children.length; i++) {
 514  1515
                                 NodeList matchingElements = datatypeElement.getElementsByTagName(makeElementName(
 515  
                                                 datatypeObject, i + 1));
 516  1515
                                 if (matchingElements.getLength() > 0) {
 517  405
                                         parse(children[i], (Element) matchingElements.item(0)); // components don't
 518  
                                                                                                                                                         // repeat - use 1st
 519  
                                 }
 520  
                         }
 521  
                 }
 522  237
         }
 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  3589
                 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  3018
                 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  1753
                 boolean hasData = false;
 542  1753
                 if (datatypeObject instanceof Varies) {
 543  7
                         hasData = encodeVaries((Varies) datatypeObject, datatypeElement);
 544  1746
                 } else if (datatypeObject instanceof Primitive) {
 545  1440
                         hasData = encodePrimitive((Primitive) datatypeObject, datatypeElement);
 546  306
                 } else if (datatypeObject instanceof Composite) {
 547  306
                         hasData = encodeComposite((Composite) datatypeObject, datatypeElement);
 548  
                 }
 549  1753
                 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  7
                 boolean hasData = false;
 559  7
                 if (datatypeObject.getData() != null) {
 560  7
                         hasData = encode(datatypeObject.getData(), datatypeElement);
 561  
                 }
 562  7
                 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  1440
                 String value = datatypeObject.getValue();
 573  1440
                 boolean hasValue = (value != null && value.length() > 0);
 574  1440
                 if (hasValue) {
 575  
                         try {
 576  323
                                 EncodingCharacters ec = EncodingCharacters.getInstance(datatypeObject.getMessage());
 577  323
                                 char esc = ec.getEscapeCharacter();
 578  
                                 int pos;
 579  323
                                 int oldpos = 0;
 580  323
                                 boolean escaping = false;
 581  
 
 582  
                                 // Find next escape character
 583  359
                                 while ((pos = value.indexOf(esc, oldpos)) >= 0) {
 584  
 
 585  
                                         // string until next escape character
 586  36
                                         String v = value.substring(oldpos, pos);
 587  36
                                         if (!escaping) {
 588  
                                                 // currently in "text mode", so create textnode from it
 589  27
                                                 if (v.length() > 0)
 590  25
                                                         datatypeElement.appendChild(datatypeElement.getOwnerDocument()
 591  
                                                                         .createTextNode(v));
 592  27
                                                 escaping = true;
 593  
                                         } else {
 594  9
                                                 if (v.startsWith(".") || "H".equals(v) || "N".equals(v)) {
 595  
                                                         // currently in "escape mode", so create escape element from it
 596  8
                                                         Element escape = datatypeElement.getOwnerDocument().createElement(
 597  
                                                                         ESCAPE_NODENAME);
 598  8
                                                         escape.setAttribute(ESCAPE_ATTRNAME, v);
 599  8
                                                         datatypeElement.appendChild(escape);
 600  8
                                                         escaping = false;
 601  8
                                                 } else {
 602  
                                                         // no proper escape sequence, assume text
 603  1
                                                         datatypeElement.appendChild(datatypeElement.getOwnerDocument()
 604  
                                                                         .createTextNode(esc + v));
 605  
                                                 }
 606  
                                         }
 607  36
                                         oldpos = pos + 1;
 608  36
                                 }
 609  
                                 // create text from the remainder
 610  323
                                 if (oldpos < value.length()) {
 611  
 
 612  322
                                         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  322
                                         if (escaping)
 616  19
                                                 sb.append(esc);
 617  
 
 618  322
                                         sb.append(value.substring(oldpos));
 619  322
                                         datatypeElement.appendChild(datatypeElement.getOwnerDocument().createTextNode(
 620  
                                                         sb.toString()));
 621  
                                 }
 622  
 
 623  0
                         } catch (Exception e) {
 624  0
                                 throw new DataTypeException("Exception encoding Primitive: ", e);
 625  323
                         }
 626  
 
 627  
                 }
 628  1440
                 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  306
                 Type[] components = datatypeObject.getComponents();
 639  306
                 boolean hasValue = false;
 640  1809
                 for (int i = 0; i < components.length; i++) {
 641  1503
                         String name = makeElementName(datatypeObject, i + 1);
 642  1503
                         Element newNode = datatypeElement.getOwnerDocument().createElement(name);
 643  1503
                         boolean componentHasValue = encode(components[i], newNode);
 644  1503
                         if (componentHasValue) {
 645  
                                 try {
 646  231
                                         datatypeElement.appendChild(newNode);
 647  0
                                 } catch (DOMException e) {
 648  0
                                         throw new DataTypeException("DOMException encoding Composite: ", e);
 649  231
                                 }
 650  231
                                 hasValue = true;
 651  
                         }
 652  
                 }
 653  306
                 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  1
                 String version = getVersion(message);
 674  1
                 Segment criticalData = Parser.makeControlMSH(version, getFactory());
 675  
 
 676  1
                 Terser.set(criticalData, 1, 0, 1, 1, parseLeaf(message, "MSH.1", 0));
 677  1
                 Terser.set(criticalData, 2, 0, 1, 1, parseLeaf(message, "MSH.2", 0));
 678  1
                 Terser.set(criticalData, 10, 0, 1, 1, parseLeaf(message, "MSH.10", 0));
 679  1
                 String procID = parseLeaf(message, "MSH.11", 0);
 680  1
                 if (procID == null || procID.length() == 0) {
 681  0
                         procID = parseLeaf(message, "PT.1", message.indexOf("MSH.11"));
 682  
                         // this field is a composite in later versions
 683  
                 }
 684  1
                 Terser.set(criticalData, 11, 0, 1, 1, procID);
 685  
 
 686  1
                 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  7
                 String ackID = null;
 700  
                 try {
 701  7
                         ackID = parseLeaf(message, "msa.2", 0).trim();
 702  3
                 } catch (HL7Exception e) { /* OK ... assume it isn't a response message */
 703  4
                 }
 704  7
                 return ackID;
 705  
         }
 706  
 
 707  
         public String getVersion(String message) throws HL7Exception {
 708  12
         String version = parseLeaf(message, "MSH.12", 0);
 709  12
         if (version == null || version.trim().length() == 0) {
 710  10
             version = parseLeaf(message, "VID.1", message.indexOf("MSH.12"));
 711  
         }
 712  12
         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  33
                 int tagStart = message.indexOf("<" + tagName, startAt);
 731  33
                 if (tagStart < 0)
 732  5
                         tagStart = message.indexOf("<" + tagName.toUpperCase(), startAt);
 733  33
                 int valStart = message.indexOf(">", tagStart) + 1;
 734  33
                 int valEnd = message.indexOf("<", valStart);
 735  
 
 736  33
                 if (tagStart >= 0 && valEnd >= valStart) {
 737  30
                         value = message.substring(valStart, valEnd);
 738  
                 } else {
 739  3
                         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  30
                 value = value.replaceAll("&quot;", "\"");
 746  30
                 value = value.replaceAll("&apos;", "'");
 747  30
                 value = value.replaceAll("&amp;", "&");
 748  30
                 value = value.replaceAll("&lt;", "<");
 749  30
                 value = value.replaceAll("&gt;", ">");
 750  
 
 751  30
                 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  0
                 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  0
                 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  0
                 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  0
                 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  0
                 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  0
                 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  0
                 this.textEncoding = textEncoding;
 826  0
         }
 827  
 
 828  
 }