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