All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.uhn.hl7v2.parser.XMLParser Maven / Gradle / Ivy

There is a newer version: 2.5.1
Show newest version
/**
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.mozilla.org/MPL/
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
 * specific language governing rights and limitations under the License.
 *
 * The Original Code is "XMLParser.java".  Description:
 * "Parses and encodes HL7 messages in XML form, according to HL7's normative XML encoding
 * specification."
 *
 * The Initial Developer of the Original Code is University Health Network. Copyright (C)
 * 2002.  All Rights Reserved.
 *
 * Contributor(s): ______________________________________.
 *
 * Alternatively, the contents of this file may be used under the terms of the
 * GNU General Public License (the  �GPL�), in which case the provisions of the GPL are
 * applicable instead of those above.  If you wish to allow use of your version of this
 * file only under the terms of the GPL and not to allow others to use your version
 * of this file under the MPL, indicate your decision by deleting  the provisions above
 * and replace  them with the notice and other provisions required by the GPL License.
 * If you do not delete the provisions above, a recipient may use your version of
 * this file under either the MPL or the GPL.
 */

package ca.uhn.hl7v2.parser;

import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ca.uhn.hl7v2.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import ca.uhn.hl7v2.ErrorCode;
import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.HapiContext;
import ca.uhn.hl7v2.model.Composite;
import ca.uhn.hl7v2.model.DataTypeException;
import ca.uhn.hl7v2.model.GenericComposite;
import ca.uhn.hl7v2.model.GenericMessage;
import ca.uhn.hl7v2.model.GenericPrimitive;
import ca.uhn.hl7v2.model.Message;
import ca.uhn.hl7v2.model.Primitive;
import ca.uhn.hl7v2.model.Segment;
import ca.uhn.hl7v2.model.Type;
import ca.uhn.hl7v2.model.Varies;
import ca.uhn.hl7v2.util.Terser;
import ca.uhn.hl7v2.util.XMLUtils;

/**
 * Parses and encodes HL7 messages in XML form, according to HL7's normative XML encoding
 * specification. This is an abstract class that handles datatype and segment parsing/encoding, but
 * not the parsing/encoding of entire messages. To use the XML parser, you should create a subclass
 * for a certain message structure. This subclass must be able to identify the Segment objects that
 * correspond to various Segment nodes in an XML document, and call the methods 
 * parse(Segment segment, ElementNode segmentNode) and
 * encode(Segment segment, ElementNode segmentNode)
 *  as appropriate. XMLParser uses the Xerces parser, which must be installed in your
 * classpath.
 * 
 * @see ParserConfiguration for configuration options which may affect parser encoding and decoding behaviour
 * @author Bryan Tripp, Shawn Bellina
 */
public abstract class XMLParser extends Parser {

	private static final String ESCAPE_ATTRNAME = "V";
	private static final String ESCAPE_NODENAME = "escape";
	private static final Logger log = LoggerFactory.getLogger(XMLParser.class);
    protected static final String NS = "urn:hl7-org:v2xml";
    private static final Pattern NS_PATTERN = Pattern.compile("xmlns(.*)=\"" + NS + "\"");

	private String textEncoding;




	/** Constructor */
	public XMLParser() {
		super();
	}

    /**
     *
     * @param context the HAPI context
     */
	public XMLParser(HapiContext context) {
		super(context);
	}

	/**
	 * Constructor
	 * 
	 * @param theFactory custom factory to use for model class lookup
	 */
	public XMLParser(ModelClassFactory theFactory) {
		super(theFactory);

	}

	/**
	 * Returns a String representing the encoding of the given message, if the encoding is
	 * recognized. For example if the given message appears to be encoded using HL7 2.x XML rules
	 * then "XML" would be returned. If the encoding is not recognized then null is returned. That
	 * this method returns a specific encoding does not guarantee that the message is correctly
	 * encoded (e.g. well formed XML) - just that it is not encoded using any other encoding than
	 * the one returned. Returns null if the encoding is not recognized.
	 */
	public String getEncoding(String message) {
		return EncodingDetector.isXmlEncoded(message) ? getDefaultEncoding() : null;
	}

	/**
	 * @return the preferred encoding of this Parser
	 */
	public String getDefaultEncoding() {
		return "XML";
	}

	/**
	 * Sets the keepAsOriginalNodes
	 * 
	 * The nodes whose names match the keepAsOriginalNodes will be kept as original, meaning
	 * that no white space treaming will occur on them
     *
     * @param keepAsOriginalNodes of the nodes to be kept as original
     * @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. 
	 */
	@Deprecated()
	public void setKeepAsOriginalNodes(String[] keepAsOriginalNodes) {
		getParserConfiguration().setXmlDisableWhitespaceTrimmingOnNodeNames(keepAsOriginalNodes);
	}

	/**
	 * Sets the keepAsOriginalNodes
	 * 
	 * @deprecated Use {@link ParserConfiguration#getXmlDisableWhitespaceTrimmingOnNodeNames()} instead
	 */
	@Deprecated
	public String[] getKeepAsOriginalNodes() {
		return getParserConfiguration().getXmlDisableWhitespaceTrimmingOnNodeNames().toArray(new String[getParserConfiguration().getXmlDisableWhitespaceTrimmingOnNodeNames().size()]);
	}

	/**
	 * 

* Creates and populates a Message object from an XML Document that contains an XML-encoded HL7 * message. *

*

* The easiest way to implement this method for a particular message structure is as follows: *

    *
  1. Create an instance of the Message type you are going to handle with your subclass of * XMLParser
  2. *
  3. Go through the given Document and find the Elements that represent the top level of each * message segment.
  4. *
  5. For each of these segments, call * parse(Segment segmentObject, Element segmentElement), providing the appropriate * Segment from your Message object, and the corresponding Element.
  6. *
* At the end of this process, your Message object should be populated with data from the XML * Document. *

* * @param xmlMessage DOM message object to be parsed * @param version HL7 version * @throws HL7Exception if the message is not correctly formatted. * @throws EncodingNotSupportedException if the message encoded is not supported by this parser. */ public abstract Message parseDocument(Document xmlMessage, String version) throws HL7Exception; /** *

* Parses a message string and returns the corresponding Message object. This method checks that * the given message string is XML encoded, creates an XML Document object (using Xerces) from * the given String, and calls the abstract method parse(Document XMLMessage) *

*/ protected Message doParse(String message, String version) throws HL7Exception { Message m; // parse message string into a DOM document Document doc; doc = parseStringIntoDocument(message); m = parseDocument(doc, version); return m; } /** * Parses a string containing an XML document into a Document object. * * Note that this method is synchronized currently, as the XML parser is not thread safe * * @throws HL7Exception */ protected synchronized Document parseStringIntoDocument(String message) throws HL7Exception { try { return XMLUtils.parse(message); } catch (Exception e) { throw new HL7Exception("Exception parsing XML", e); } } /** * Formats a Message object into an HL7 message string using the given encoding. * * @throws HL7Exception if the data fields in the message do not permit encoding (e.g. required * fields are null) * @throws EncodingNotSupportedException if the requested encoding is not supported by this * parser. */ protected String doEncode(Message source, String encoding) throws HL7Exception { if (!encoding.equals("XML")) throw new EncodingNotSupportedException("XMLParser supports only XML encoding"); return encode(source); } /** * Formats a Message object into an HL7 message string using this parser's default encoding (XML * encoding). This method calls the abstract method encodeDocument(...) in order to * obtain XML Document object representation of the Message, then serializes it to a String. * * @throws HL7Exception if the data fields in the message do not permit encoding (e.g. required * fields are null) */ protected String doEncode(Message source) throws HL7Exception { if (source instanceof GenericMessage) { throw new HL7Exception( "Can't XML-encode a GenericMessage. Message must have a recognized structure."); } Document doc = encodeDocument(source); // Element documentElement = doc.getDocumentElement(); // if (!documentElement.hasAttribute("xmlns")) // documentElement.setAttribute("xmlns", "urn:hl7-org:v2xml"); try { return XMLUtils.serialize(doc, getParserConfiguration().isPrettyPrintWhenEncodingXml()); } catch (Exception e) { throw new HL7Exception("Exception serializing XML document to string", e); } } /** *

* Creates an XML Document that corresponds to the given Message object. *

*

* If you are implementing this method, you should create an XML Document, and insert XML * Elements into it that correspond to the groups and segments that belong to the message type * that your subclass of XMLParser supports. Then, for each segment in the message, call the * method encode(Segment segmentObject, Element segmentElement) using the Element * for that segment and the corresponding Segment object from the given Message. *

* * @param source message * @return the DOM document object of the encoded message */ public abstract Document encodeDocument(Message source) throws HL7Exception; protected void assertNamespaceURI(String ns) throws HL7Exception { if (!NS.equals(ns)) { throw new HL7Exception("Namespace URI must be " + NS); } } /** * Populates the given Segment object with data from the given XML Element. * * @param segmentObject the segment to parse into * @param segmentElement the DOM element to be parsed * @throws HL7Exception if the XML Element does not have the correct name and structure for the * given Segment, or if there is an error while setting individual field values. */ public void parse(Segment segmentObject, Element segmentElement) throws HL7Exception { Set done = new HashSet(); NodeList all = segmentElement.getChildNodes(); for (int i = 0; i < all.getLength(); i++) { String elementName = all.item(i).getNodeName(); if (all.item(i).getNodeType() == Node.ELEMENT_NODE && !done.contains(elementName)) { assertNamespaceURI(all.item(i).getNamespaceURI()); done.add(elementName); int index = elementName.indexOf('.'); if (index >= 0 && elementName.length() > index) { // properly formatted element String fieldNumString = elementName.substring(index + 1); int fieldNum = Integer.parseInt(fieldNumString); parseReps(segmentObject, segmentElement, elementName, fieldNum); } else { log.debug("Child of segment {} doesn't look like a field {}", segmentObject.getName(), elementName); } } } // set data type of OBX-5 if (segmentObject.getClass().getName().contains("OBX")) { FixFieldDataType.fixOBX5(segmentObject, getFactory(), getHapiContext().getParserConfiguration()); } // set data type of MFE-4 if (segmentObject.getClass().getName().contains("MFE") && Version.versionOf(segmentObject.getMessage().getVersion()).isGreaterThan(Version.V23)) { FixFieldDataType.fixMFE4(segmentObject, getFactory(), getHapiContext().getParserConfiguration()); } } private void parseReps(Segment segmentObject, Element segmentElement, String fieldName, int fieldNum) throws HL7Exception { NodeList reps = segmentElement.getElementsByTagName(fieldName); for (int i = 0; i < reps.getLength(); i++) { parse(segmentObject.getField(fieldNum, i), (Element) reps.item(i)); } } /** * Populates the given Element with data from the given Segment, by inserting Elements * corresponding to the Segment's fields, their components, etc. Returns true if there is at * least one data value in the segment. * * @param segmentObject the segment to be encoded * @param segmentElement the DOM element to encode into * @return true if there is at least one data value in the segment * @throws HL7Exception if an erro occurred while encoding */ public boolean encode(Segment segmentObject, Element segmentElement) throws HL7Exception { boolean hasValue = false; int n = segmentObject.numFields(); for (int i = 1; i <= n; i++) { String name = makeElementName(segmentObject, i); Type[] reps = segmentObject.getField(i); for (Type rep : reps) { Element newNode = segmentElement.getOwnerDocument().createElement(name); boolean componentHasValue = encode(rep, newNode); if (componentHasValue) { try { segmentElement.appendChild(newNode); } catch (DOMException e) { throw new HL7Exception("DOMException encoding Segment: ", e); } hasValue = true; } } } return hasValue; } /** * Populates the given Type object with data from the given XML Element. * * @param datatypeObject the type to parse into * @param datatypeElement the DOM element to be parsed * @throws DataTypeException if the data did not match the expected type rules */ public void parse(Type datatypeObject, Element datatypeElement) throws HL7Exception { if (datatypeObject instanceof Varies) { parseVaries((Varies) datatypeObject, datatypeElement); } else if (datatypeObject instanceof Primitive) { parsePrimitive((Primitive) datatypeObject, datatypeElement); } else if (datatypeObject instanceof Composite) { parseComposite((Composite) datatypeObject, datatypeElement); } } /** * Parses an XML element into a Varies by determining whether the element is primitive or * composite, calling setData() on the Varies with a new generic primitive or composite as * appropriate, and then calling parse again with the new Type object. */ private void parseVaries(Varies datatypeObject, Element datatypeElement) throws HL7Exception { // figure out what data type it holds // short nodeType = datatypeElement.getFirstChild().getNodeType(); if (!hasChildElement(datatypeElement)) { // it's a primitive datatypeObject.setData(new GenericPrimitive(datatypeObject.getMessage())); } else { // it's a composite ... almost know what type, except that we don't have the version // here datatypeObject.setData(new GenericComposite(datatypeObject.getMessage())); } parse(datatypeObject.getData(), datatypeElement); } /** Returns true if any of the given element's children are (non-escape) elements */ private boolean hasChildElement(Element e) { NodeList children = e.getChildNodes(); boolean hasElement = false; int c = 0; while (c < children.getLength() && !hasElement) { if (children.item(c).getNodeType() == Node.ELEMENT_NODE && !ESCAPE_NODENAME.equals(children.item(c).getNodeName())) { hasElement = true; } c++; } return hasElement; } /** * Parses a primitive type by filling it with text child, if any. If the datatype element * contains escape elements, resolve them properly. */ private void parsePrimitive(Primitive datatypeObject, Element datatypeElement) throws HL7Exception { NodeList children = datatypeElement.getChildNodes(); StringBuilder builder = new StringBuilder(); for (int c = 0; c < children.getLength(); c++) { Node child = children.item(c); try { if (child.getNodeType() == Node.TEXT_NODE) { String value = child.getNodeValue(); if (value != null && value.length() > 0) { if (keepAsOriginal(child.getParentNode())) { builder.append(value); } else { builder.append(removeWhitespace(value)); } } // Check for formatting elements } else if (child.getNodeType() == Node.ELEMENT_NODE && ESCAPE_NODENAME.equals(child.getLocalName())) { assertNamespaceURI(child.getNamespaceURI()); EncodingCharacters ec = EncodingCharacters.getInstance(datatypeObject .getMessage()); Element elem = (Element) child; String attr = elem.getAttribute(ESCAPE_ATTRNAME).trim(); if (attr.length() > 0) { builder.append(ec.getEscapeCharacter()).append(attr) .append(ec.getEscapeCharacter()); } } } catch (Exception e) { log.error("Error parsing primitive value from TEXT_NODE", e); } } datatypeObject.setValue(builder.toString()); } /** * Checks if Node content should be kept as original (ie.: whitespaces won't be * removed) * * @param node The target Node * @return boolean true if whitespaces should not be removed from node content, * false otherwise */ protected boolean keepAsOriginal(Node node) { if (getParserConfiguration().isXmlDisableWhitespaceTrimmingOnAllNodes()) { return true; } return (node.getNodeName() != null) && getParserConfiguration().getXmlDisableWhitespaceTrimmingOnNodeNames().contains(node.getNodeName()); } /** * Removes all unnecessary whitespace from the given String (intended to be used with Primitive * values). This includes leading and trailing whitespace, and repeated space characters. * Carriage returns, line feeds, and tabs are replaced with spaces. */ protected String removeWhitespace(String s) { s = s.replace('\r', ' '); s = s.replace('\n', ' '); s = s.replace('\t', ' '); boolean repeatedSpacesExist = true; while (repeatedSpacesExist) { int loc = s.indexOf(" "); if (loc < 0) { repeatedSpacesExist = false; } else { StringBuilder buf = new StringBuilder(); buf.append(s.substring(0, loc)); buf.append(" "); buf.append(s.substring(loc + 2)); s = buf.toString(); } } return s.trim(); } /** * Populates a Composite type by looping through it's children, finding corresponding Elements * among the children of the given Element, and calling parse(Type, Element) for each. */ private void parseComposite(Composite datatypeObject, Element datatypeElement) throws HL7Exception { if (datatypeObject instanceof GenericComposite) { // elements won't be named // GenericComposite.x NodeList children = datatypeElement.getChildNodes(); int compNum = 0; for (int i = 0; i < children.getLength(); i++) { if (children.item(i).getNodeType() == Node.ELEMENT_NODE) { Element nextElement = (Element) children.item(i); assertNamespaceURI(nextElement.getNamespaceURI()); String localName = nextElement.getLocalName(); int dotIndex = localName.indexOf("."); if (dotIndex > -1) { compNum = Integer.parseInt(localName.substring(dotIndex + 1)) - 1; } else { log.debug( "Datatype element {} doesn't have a valid numbered name, usgin default index of {}", datatypeElement.getLocalName(), compNum); } Type nextComponent = datatypeObject.getComponent(compNum); parse(nextComponent, nextElement); compNum++; } } } else { Type[] children = datatypeObject.getComponents(); for (int i = 0; i < children.length; i++) { NodeList matchingElements = datatypeElement.getElementsByTagNameNS(NS, makeElementName( datatypeObject, i + 1)); if (matchingElements.getLength() > 0) { parse(children[i], (Element) matchingElements.item(0)); } } int nextExtraCmpIndex = 0; boolean foundExtraComponent; do { foundExtraComponent = false; NodeList matchingElements = datatypeElement.getElementsByTagNameNS(NS, makeElementName(datatypeObject, children.length + nextExtraCmpIndex + 1)); if (matchingElements.getLength() > 0) { parse(datatypeObject.getExtraComponents().getComponent(nextExtraCmpIndex), (Element) matchingElements.item(0)); foundExtraComponent = true; } nextExtraCmpIndex++; } while (foundExtraComponent); } } /** Returns the expected XML element name for the given child of the given Segment */ private String makeElementName(Segment s, int child) { return s.getName() + "." + child; } /** Returns the expected XML element name for the given child of the given Composite */ private String makeElementName(Composite composite, int child) { return composite.getName() + "." + child; } /** * Populates the given Element with data from the given Type, by inserting Elements * corresponding to the Type's components and values. Returns true if the given type contains a * value (i.e. for Primitives, if getValue() doesn't return null, and for Composites, if at * least one underlying Primitive doesn't return null). */ private boolean encode(Type datatypeObject, Element datatypeElement) throws DataTypeException { boolean hasData = false; if (datatypeObject instanceof Varies) { hasData = encodeVaries((Varies) datatypeObject, datatypeElement); } else if (datatypeObject instanceof Primitive) { hasData = encodePrimitive((Primitive) datatypeObject, datatypeElement); } else if (datatypeObject instanceof Composite) { hasData = encodeComposite((Composite) datatypeObject, datatypeElement); } return hasData; } /** * Encodes a Varies type by extracting it's data field and encoding that. Returns true if the * data field (or one of its components) contains a value. */ private boolean encodeVaries(Varies datatypeObject, Element datatypeElement) throws DataTypeException { boolean hasData = false; if (datatypeObject.getData() != null) { hasData = encode(datatypeObject.getData(), datatypeElement); } return hasData; } /** * Encodes a Primitive in XML by adding it's value as a child of the given Element. Detects * escape character and creates proper elements in the DOM tree. Returns true if the * given Primitive contains a value. */ private boolean encodePrimitive(Primitive datatypeObject, Element datatypeElement) throws DataTypeException { String value = datatypeObject.getValue(); boolean hasValue = (value != null && value.length() > 0); if (hasValue) { try { EncodingCharacters ec = EncodingCharacters.getInstance(datatypeObject.getMessage()); char esc = ec.getEscapeCharacter(); int pos; int oldpos = 0; boolean escaping = false; // Find next escape character while ((pos = value.indexOf(esc, oldpos)) >= 0) { // string until next escape character String v = value.substring(oldpos, pos); if (!escaping) { // currently in "text mode", so create textnode from it if (v.length() > 0) datatypeElement.appendChild(datatypeElement.getOwnerDocument() .createTextNode(v)); escaping = true; } else { if (v.startsWith(".") || "H".equals(v) || "N".equals(v)) { // currently in "escape mode", so create escape element from it Element escape = datatypeElement.getOwnerDocument().createElement( ESCAPE_NODENAME); escape.setAttribute(ESCAPE_ATTRNAME, v); datatypeElement.appendChild(escape); escaping = false; } else { // no proper escape sequence, assume text datatypeElement.appendChild(datatypeElement.getOwnerDocument() .createTextNode(esc + v)); } } oldpos = pos + 1; } // create text from the remainder if (oldpos <= value.length()) { StringBuilder sb = new StringBuilder(); // If we are in escaping mode, there appears no closing escape character, // so we treat the string as text if (escaping) sb.append(esc); sb.append(value.substring(oldpos)); datatypeElement.appendChild(datatypeElement.getOwnerDocument().createTextNode( sb.toString())); } } catch (Exception e) { throw new DataTypeException("Exception encoding Primitive: ", e); } } return hasValue; } /** * Encodes a Composite in XML by looping through it's components, creating new children for each * of them (with the appropriate names) and populating them by calling encode(Type, Element) * using these children. Returns true if at least one component contains a value. */ private boolean encodeComposite(Composite datatypeObject, Element datatypeElement) throws DataTypeException { Type[] components = datatypeObject.getComponents(); boolean hasValue = false; for (int i = 0; i < components.length; i++) { String name = makeElementName(datatypeObject, i + 1); Element newNode = datatypeElement.getOwnerDocument().createElement(name); boolean componentHasValue = encode(components[i], newNode); if (componentHasValue) { try { datatypeElement.appendChild(newNode); } catch (DOMException e) { throw new DataTypeException("DOMException encoding Composite: ", e); } hasValue = true; } } return hasValue; } /** *

* Returns a minimal amount of data from a message string, including only the data needed to * send a response to the remote system. This includes the following fields: *

    *
  • field separator
  • *
  • encoding characters
  • *
  • processing ID
  • *
  • message control ID
  • *
* This method is intended for use when there is an error parsing a message, (so the Message * object is unavailable) but an error message must be sent back to the remote system including * some of the information in the inbound message. This method parses only that required * information, hopefully avoiding the condition that caused the original error. *

*/ public Segment getCriticalResponseData(String message) throws HL7Exception { String version = getVersion(message); Segment criticalData = Parser.makeControlMSH(version, getFactory()); Terser.set(criticalData, 1, 0, 1, 1, parseLeaf(message, "MSH.1", 0)); Terser.set(criticalData, 2, 0, 1, 1, parseLeaf(message, "MSH.2", 0)); Terser.set(criticalData, 10, 0, 1, 1, parseLeaf(message, "MSH.10", 0)); String procID = parseLeaf(message, "MSH.11", 0); if (procID == null || procID.length() == 0) { procID = parseLeaf(message, "PT.1", message.indexOf("MSH.11")); // this field is a composite in later versions } Terser.set(criticalData, 11, 0, 1, 1, procID); return criticalData; } /** * For response messages, returns the value of MSA-2 (the message ID of the message sent by the * sending system). This value may be needed prior to main message parsing, so that * (particularly in a multi-threaded scenario) the message can be routed to the thread that sent * the request. We need this information first so that any parse exceptions are thrown to the * correct thread. Implementers of Parsers should take care to make the implementation of this * method very fast and robust. Returns null if MSA-2 can not be found (e.g. if the message is * not a response message). Trims whitespace from around the MSA-2 field. */ public String getAckID(String message) { String ackID = null; try { ackID = parseLeaf(message, "msa.2", 0).trim(); } catch (HL7Exception e) { /* OK ... assume it isn't a response message */ } return ackID; } public String getVersion(String message) throws HL7Exception { String version = parseLeaf(message, "MSH.12", 0); if (version == null || version.trim().length() == 0) { version = parseLeaf(message, "VID.1", message.indexOf("MSH.12")); } return version; } /** * Attempts to retrieve the value of a leaf tag without using DOM or SAX. This method searches * the given message string for the given tag name, and returns everything after the given tag * and before the start of the next tag. Whitespace is stripped. This is intended only for lead * nodes, as the value is considered to end at the start of the next tag, regardless of whether * it is the matching end tag or some other nested tag. * * @param message a string message in XML form * @param tagName the name of the XML tag, e.g. "MSA.2" * @param startAt the character location at which to start searching * @throws HL7Exception if the tag can not be found */ protected static String parseLeaf(String message, String tagName, int startAt) throws HL7Exception { // Workaround #176: XML may include explicit namespaces. It would be more stable to use some // kind of pull parser for this method instead of manually digging for tags in the XML structure. String prefix = ""; Matcher m = NS_PATTERN.matcher(message); if (m.find()) { String ns = m.group(1); if (ns != null && ns.length() > 0) { prefix = ns.substring(1) + ":"; } } int tagStart = message.indexOf("<" + prefix + tagName, startAt); if (tagStart < 0) tagStart = message.indexOf("<" + prefix + tagName.toUpperCase(), startAt); int valStart = message.indexOf(">", tagStart) + 1; int valEnd = message.indexOf("<", valStart); String value; if (tagStart >= 0 && valEnd >= valStart) { value = message.substring(valStart, valEnd); } else { throw new HL7Exception("Couldn't find " + tagName + " in message beginning: " + message.substring(0, Math.min(150, message.length())), ErrorCode.REQUIRED_FIELD_MISSING); } // Escape codes, as defined at http://hdf.ncsa.uiuc.edu/HDF5/XML/xml_escape_chars.htm value = value.replaceAll(""", "\""); value = value.replaceAll("'", "'"); value = value.replaceAll("&", "&"); value = value.replaceAll("<", "<"); value = value.replaceAll(">", ">"); return value; } /** * Throws unsupported operation exception * * @throws UnsupportedOperationException */ @Override public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception { throw new UnsupportedOperationException("Not supported yet."); } /** * Throws unsupported operation exception * * @throws UnsupportedOperationException */ @Override protected Message doParseForSpecificPackage(String theMessage, String theVersion, String thePackageName) throws HL7Exception { throw new UnsupportedOperationException("Not supported yet."); } /** * Throws unsupported operation exception * * @throws UnsupportedOperationException */ @Override public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception { throw new UnsupportedOperationException("Not supported yet."); } /** * Throws unsupported operation exception * * @throws UnsupportedOperationException */ @Override public void parse(Type type, String string, EncodingCharacters encodingCharacters) throws HL7Exception { throw new UnsupportedOperationException("Not supported yet."); } /** * Throws unsupported operation exception * * @throws UnsupportedOperationException */ @Override public void parse(Segment segment, String string, EncodingCharacters encodingCharacters) throws HL7Exception { throw new UnsupportedOperationException("Not supported yet."); } /** * Returns the text encoding to be used in generating new messages. Note that this affects * encoding to string only, not parsing. * * @return text encoding */ public String getTextEncoding() { return textEncoding; } /** * Sets the text encoding to be used in generating new messages. Note that this affects encoding * to string only, not parsing. * * @param textEncoding The encoding. Default is the platform default. */ public void setTextEncoding(String textEncoding) { this.textEncoding = textEncoding; } }