View Javadoc

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  	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  	private String concatKeepAsOriginalNodes = "";
88  
89  	/** Constructor */
90  	public XMLParser() {
91  		super();
92  	}
93  
94      /**
95       *
96       * @param context the HAPI context
97       */
98  	public XMLParser(HapiContext context) {
99  		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("&quot;", "\"");
746 		value = value.replaceAll("&apos;", "'");
747 		value = value.replaceAll("&amp;", "&");
748 		value = value.replaceAll("&lt;", "<");
749 		value = value.replaceAll("&gt;", ">");
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 }