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