XMLTranslatorStream to the rescue

This utility class found in the Microsoft.BizTalk.Streaming namespace has saved me on many occasions where I had to modify namespaces, elements, attributes and the XML declarations inside a pipeline and from a referenced custom assemble inside an orchestration. This class uses a stream reader and writer with virtual methods representing the components of a xml structure.

More information can be found here: http://msdn.microsoft.com/en-us/library/microsoft.biztalk.streaming.xmltranslatorstream.aspx

Using the XMLtranslatorStream avoids having to load the xml message into a xmlDocument object which will impact performance for large messages.

The list below shows all the available methods that may be overridden within this class

protected override int ProcessXmlNodes(int count);
 protected virtual void TranslateAttribute();
 protected virtual void TranslateAttributes();
 protected virtual void TranslateAttributeValue(string prefix, string localName, string nsURI, string val);
 protected virtual void TranslateCData(string data);
 protected virtual void TranslateComment(string comment);
 protected virtual void TranslateDocType(string name, string pubAttr, string systemAttr, string subset);
 protected virtual void TranslateElement();
 protected virtual void TranslateEndElement(bool full);
 protected virtual void TranslateEntityRef(string name);
 protected virtual void TranslateProcessingInstruction(string target, string val);
 protected virtual void TranslateStartAttribute(string prefix, string localName, string nsURI);
 protected virtual void TranslateStartElement(string prefix, string localName, string nsURI);
 protected virtual void TranslateText(string s);
 protected virtual void TranslateWhitespace(string space);
 protected virtual void TranslateXmlDeclaration(string target, string val);
 protected virtual bool TranslateXmlNode();

 

 

To use the XmlTranslatorStream inside a custom pipeline, add a subclass that inherits the XmlTranslatorStream. An example is shown below where I have overridden some of the methods.

Extra information maybe passed to the subclass by adding parameters to the constructor.

#region Subclass XmlModifierStream
    public class XmlModifierStream : XmlTranslatorStream
    {
        public XmlModifierStream(Stream input): base(new XmlTextReader(input), Encoding.Default)
        {
            Debug.WriteLine("[BTS.Utilities.CustomPipelines.XmlNamespaceModifierStream]Entered method");
        }

        protected override void TranslateXmlDeclaration(string target, string val)
        {
            base.TranslateXmlDeclaration(target, val);            
        }

        protected override void TranslateStartElement(string prefix, string localName, string nsURI)
        {            
            base.TranslateStartElement(prefix, localName, nsURI);
        }

        protected override void TranslateStartAttribute(string prefix, string localName, string nsURI)
        {            
            base.TranslateStartAttribute(prefix, localName, nsURI);    
        }         
    }
#endregion

 

 

Then inside the Execute method of the pipeline call the constructor of the sub class passing the data stream as shown below.

public Microsoft.BizTalk.Message.Interop.IBaseMessage Execute(
          Microsoft.BizTalk.Component.Interop.IPipelineContext pc, Microsoft.BizTalk.Message.Interop.IBaseMessage inmsg)
      {
          Debug.WriteLine("[BTS.Utilities.CustomPipelines.Execute]Entered method");

          if (inmsg == null || inmsg.BodyPart == null || inmsg.BodyPart.Data == null)
          {
              throw new ArgumentNullException("inmsg");
          }

          //call the xml translator subclass
          inmsg.BodyPart.Data = new XmlModifierStream(inmsg.BodyPart.GetOriginalDataStream());

          Debug.WriteLine("[BTS.Utilities.CustomPipelines.Execute]Exit method");
          return inmsg;
      }

 

 

Below are some examples where I have used the XMLTranslatorStream class.

In this scenario the incoming message contained elements which specified the document version and document type. The values TypeVersion and Type were read from the message below and used to identify the message type and finally rename the root element, remove all the element prefixes and update the namespace.

image

Inside the Execute function of the pipeline, I first opened a stream to obtain the required values from the message. These values are then passed as parameters to the subclass XmlNamespaceModifierStream together with a reference to the original stream.

The subclass overrides the TranslateStartElement method and checks if the current element is the root element of the document. If it is the root element I rename the element from StandardBusinessDocument to either CatalogueItemNotification or PriceSynchronisationDocument and set the namespace to a new value.

The overiden TranslateAttribute removes any attributtes that are prefixed with “xmlns”

//xpath the the document version elelment

private const string XPATHQUERY_VERSION = "/*[local-name()='StandardBusinessDocument']/*[local-name()='StandardBusinessDocumentHeader']/*[local-name()='DocumentIdentification']/*[local-name()='TypeVersion']";
private const string DOCUMENTTYPE_ELEMENT = "Type";




#region IComponent members
public Microsoft.BizTalk.Message.Interop.IBaseMessage Execute(Microsoft.BizTalk.Component.Interop.IPipelineContext pc, Microsoft.BizTalk.Message.Interop.IBaseMessage inmsg)
{
Debug.WriteLine("[BTS.Utilities.CustomPipelines.NamespaceModifier.Execute]Entered method");

if (inmsg == null || inmsg.BodyPart == null || inmsg.BodyPart.Data == null)
{
throw new ArgumentNullException("inmsg");
}

string propertyVersionValue = string.Empty;
string propertyTypeValue = string.Empty;

if (!string.IsNullOrEmpty(XPATHQUERY_VERSION))
{
Debug.WriteLine("[BTS.Utilities.CustomPipelines.NamespaceModifier.Execute]Obtain the xpath value from within the document");
IBaseMessagePart bodyPart = inmsg.BodyPart;
Stream inboundStream = bodyPart.GetOriginalDataStream();

//note that the path would be "C:\Documents and Settings\<BTSHostInstanceName>\Local Settings\Temp" for the virtual stream
VirtualStream virtualStream = new VirtualStream(VirtualStream.MemoryFlag.AutoOverFlowToDisk);
ReadOnlySeekableStream readOnlySeekableStream = new ReadOnlySeekableStream(inboundStream, virtualStream);
XmlTextReader xmlTextReader = new XmlTextReader(readOnlySeekableStream);
XPathCollection xPathCollection = new XPathCollection();
XPathReader xPathReader = new XPathReader(xmlTextReader, xPathCollection);
xPathCollection.Add(XPATHQUERY_VERSION);
bool isFirstMatch = false;
while (xPathReader.ReadUntilMatch())
{
//only interested in the first match
if (xPathReader.Match(0) && !isFirstMatch)
{
propertyVersionValue = xPathReader.ReadString();
isFirstMatch = true;

//get the type next which is 2nd element down 
while (xPathReader.Read())
{
if (xPathReader.LocalName.Equals(DOCUMENTTYPE_ELEMENT))
{
propertyTypeValue = xPathReader.ReadString();
break;
}
}
}
}

if (isFirstMatch)
{
Debug.WriteLine(string.Format("[BTS.Utilities.CustomPipelines.NetNamespaceModifier.Execute]Match found for xpath query. Value equals:{0}", propertyVersionValue));
}
else
{
Trace.WriteLine(string.Format("[BTS.Utilities.CustomPipelines.NetNamespaceModifier.Execute]No match found for xpath query '{0}'", XPATHQUERY_VERSION));
}

//rewind back to start
readOnlySeekableStream.Position = 0;
bodyPart.Data = readOnlySeekableStream;
}

inmsg.BodyPart.Data = new XmlNamespaceModifierStream(inmsg.BodyPart.GetOriginalDataStream(), propertyVersionValue, propertyTypeValue);
Debug.WriteLine("[BTS.Utilities.CustomPipelines.NamespaceModifier.Execute]Exit method");
return inmsg;
}
#endregion

#region Subclass XmlNamespaceModifierStream
public class XmlNamespaceModifierStream : XmlTranslatorStream
{
private const string CIN_DOCTYPE = "catalogueItemNotification";
private const string CPN_DOCTYPE = "priceSynchronisationDocument";
private const string ROOT_GS1_ELEMENT = "StandardBusinessDocument";
private const string NS_PREFIX = "urn:ean.ucc:";

private string _newNamespaceVersion;
private string _documentType;

protected override void TranslateStartElement(string prefix, string localName, string nsURI)
{
string newNSUri = string.Empty;
bool isElementFoundWithNamespace = false;
bool isFirstElement = false;

if (!string.IsNullOrEmpty(prefix) && !isFirstElement)
{
//element found with prefix. Modify namespace with new value and append passed namespace version 
newNSUri = NS_PREFIX + _newNamespaceVersion;
isElementFoundWithNamespace = true;

if (localName.Equals(ROOT_GS1_ELEMENT))
isFirstElement = true;
}

if (isElementFoundWithNamespace & isFirstElement)
{
//replace with new namespace
Debug.WriteLine(string.Format("[BTS.Utilities.CustomPipelines.NamespaceModifier.XmlNamespaceModifierStream]Replace namespace with {0}", nsURI + newNSUri));

if (_documentType.Equals(CIN_DOCTYPE))
localName = localName + "Catalogue";
if (_documentType.Equals(CPN_DOCTYPE))
localName = localName + "Price";

base.TranslateStartElement(null, localName, newNSUri);
}
else
{
base.TranslateStartElement(null, localName, null);
}

}

protected override void TranslateAttribute()
{
if (this.m_reader.Prefix != "xmlns" && this.m_reader.Name != "xmlns")
base.TranslateAttribute();
}

public XmlNamespaceModifierStream(Stream input, string namespaceVersion, string documentType)
: base(new XmlTextReader(input), Encoding.Default)
{
Debug.WriteLine("[BTS.Utilities.CustomPipelines.NamespaceModifier.XmlNamespaceModifierStream]Entered method");
_newNamespaceVersion = namespaceVersion.Trim();
_documentType = documentType.Trim();
Debug.WriteLine("[BTS.Utilities.CustomPipelines.NamespaceModifier.XmlNamespaceModifierStream]Exit method");
}
}

#endregion

In this custom send pipeline I used the XMLTranslator to add the following XML declaration “version=”1.0″ encoding=”utf-8″” and to modify the namespaces and prefixes of some elements and attributes.

The Execute function in the custom pipeline component simply calls the subclass XmlExtensionModifierStream passing only the original message stream as a parameter value.

The subclass overrides the TranslateXmlDeclaration,  TranslateStartElement and TranslateStartAttribute methods to modify the values.

#region IComponent members
    public Microsoft.BizTalk.Message.Interop.IBaseMessage Execute(Microsoft.BizTalk.Component.Interop.IPipelineContext pc, 
             Microsoft.BizTalk.Message.Interop.IBaseMessage inmsg)
    {
        Debug.WriteLine("[BTS.Utilities.CustomPipelines.ExtensionModifier.Execute]Entered method");

        if (inmsg == null || inmsg.BodyPart == null || inmsg.BodyPart.Data == null)
        {
            throw new ArgumentNullException("inmsg");
        }

        inmsg.BodyPart.Data = new XmlExtensionModifierStream(inmsg.BodyPart.GetOriginalDataStream());        
        Debug.WriteLine("[BTS.Utilities.CustomPipelines.ExtensionModifier.Execute]Exit method");
        return inmsg;
    }
    
#endregion

#region Subclass XmlExtensionModifierStream
    public class XmlExtensionModifierStream : XmlTranslatorStream
    {
        public XmlExtensionModifierStream(Stream input)
            : base(new XmlTextReader(input), Encoding.Default)
        {
            Debug.WriteLine("[BTS.Utilities.CustomPipelines.ExtensionModifier.XmlNamespaceModifierStream]Entered method");
        }

        protected override void TranslateXmlDeclaration(string target, string val)
        {
            base.TranslateXmlDeclaration(target, val);
            this.m_writer.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\"");
        }

        protected override void TranslateStartElement(string prefix, string localName, string nsURI)
        {            
            switch (localName)
            {

                case "fMCGTradeItemExtension":
                    base.TranslateStartElement("fmcg", localName, "urn:ean.ucc:align:fmcg:2");
                    break;

                case "attributeValuePairExtension":
                    base.TranslateStartElement("gdsn", localName, "urn:ean.ucc:gdsn:2");
                    break;
                    
                default:
                    base.TranslateStartElement(prefix, localName, nsURI);
                    break;
            }           
        }

        protected override void TranslateStartAttribute(string prefix, string localName, string nsURI)
        {            
            switch (localName)
            {

                case "schemaLocation":
                    base.TranslateStartAttribute("xsi", localName, "http://www.w3.org/2001/XMLSchema-instance");
                    break;                

                default:
                    base.TranslateStartAttribute(prefix, localName, nsURI);
                    break;
            }
        }
         
    }
#endregion

 

 

Enjoy.

9 Replies to “XMLTranslatorStream to the rescue”

  1. Thanks for gr8 post.. I however have a requirement to enrich a received X12 EDI with a new header section withISA and GS segment and the original message together under a new node.
    Can this be achieved using XmlTranslator stream?
    Greatly appriciate the help..

    1. Hi Ritu,
      Thanks for the feedback. Yes it would be possible to use the XmlTransalatorStream to add another section under an exisiting element.

      I would look at overriding the ProcessXmlNodes() function. Within the function use the this.m_reader.LocalName to find the element you wish to extend. Then use the this.m_writer() function to create the extra section.
      Remember to flush the this.m_writer before exiting the function.

  2. Quick follow up. I’ve created a sub class that inherits from the XmlTranslatorStream. I only have one method that I needed to override (TranslateStartElement). It is much like what you have shown above. When this subclass is called from my execute method in the pipeline component, the constructor executes but the method never does. Any thoughts on why?

      1. Yes, the overrriden method does have the correct signature: void TranslateStartElement(string prefix, string localName, string nsURI)

Leave a Reply

Your email address will not be published. Required fields are marked *