AL and SOAP

This sample is based on a page extension for page 8 (Currencies) where an action for downloading exchange rates from the Swedish State Bank has been added. The logic itself isn’t super advanced and it requires SEK as local currency. The focus here is to show how to:

  • Create SOAP request XML
  • Do an HTTP Post using httpclient
  • Read the XML in the response and do something with that.

I have used a codeunit for XML DOM management created by Divesh Bora which makes things a lot easier.

I hope you find this useful and helps you on your way to move from C/AL to AL.

pageextension 57900 CurrencyListExtension extends Currencies
{
    actions
    {
        addafter(UpdateExchangeRates)
        {
            action(DownloadExchRates)
            {
                ApplicationArea = All;
                CaptionML = ENU = 'Get Exchange Rates from State Bank of Sweden', SVE = 'Hämta valutakurser från Sveriges Riksbank';
                Image = CurrencyExchangeRates;
                Promoted = true;
                PromotedCategory = Category4;

                trigger OnAction();
                var
                    DownloadExchRates: codeunit "SE State Bank Exch Rates";
                    TxtDownloadSucceeded: TextConst ENU = 'Exchange Rates updated successfully', SVE = 'Valutakurserna har uppdateras';
                    JobQueueEntry: Record "Job Queue Entry";
                begin
                    if DownloadExchRates.Run(JobQueueEntry) then
                      message(TxtDownloadSucceeded);
                end;
            }
        }
    }
}
codeunit 57900 "SE State Bank Exch Rates"
{
    // https://swea.riksbank.se/sweaWS/docs/api/call/getLatestInterestAndExchangeRates.htm
    // https://swea.riksbank.se/sweaWS/wsdl/sweaWS_ssl.wsdl

    TableNo = "Job Queue Entry";
    var
      XMLDOMMgmt: Codeunit "XML DOM Mgt.";
      SoapNS : Label 'http://www.w3.org/2003/05/soap-envelope';
      XsdNS : Label 'http://swea.riksbank.se/xsd';
      URL: label 'https://swea.riksbank.se:443/sweaWS/services/SweaWebServiceHttpSoap12Endpoint';
      NsMgr : XmlNamespaceManager;
    
    trigger OnRun();
    var 
      GLSetup : Record "General Ledger Setup";
      XMLDoc : XmlDocument;
      Currency : Record Currency;
      CurrNode : XmlNode;
      CurrNode2 : XmlNode;
      CurrNodeList : XmlNodeList;
      CurrExchRate : Record "Currency Exchange Rate";
      Unit : Decimal;
      DebugFile: File;
      OutStr: OutStream;
      NsMgr: XmlNamespaceManager;
    begin
      GLSetup.GET;
      GLSetup.TESTFIELD("LCY Code", 'SEK');
      if Currency.FINDSET then
        with XMLDOMMgmt do 
          begin
            AddRootElementWithPrefix(XMLDoc, 'Envelope', 'soap', SoapNS, CurrNode2);
            SetUtf8Declaration(XMLDoc);
            CurrNode := CurrNode2;
            AddPrefix(CurrNode, 'xsd', XsdNS);
            CurrNode := CurrNode2;
            AddElement(CurrNode, 'Header', '', SoapNs, CurrNode2);
            CurrNode := CurrNode2;
            AddElement(Currnode, 'Body', '', SoapNs, CurrNode2);
            CurrNode := CurrNode2;
            AddElement(Currnode, 'getLatestInterestAndExchangeRates', '', XsdNS, CurrNode2);
            CurrNode := CurrNode2;
            if GLOBALLANGUAGE = 1053 then
              AddElement(CurrNode, 'languageid', 'sv', '', CurrNode2)
            else
              AddElement(CurrNode, 'languageid', 'en', '', CurrNode2);
            repeat
              AddElement(CurrNode,'seriesid', GLSetup."LCY Code" + Currency.Code + 'PMI', '', CurrNode2);
            until Currency.NEXT = 0;

            HTTPPost(XMLDoc, URL);

            GetRootNode(XMLDoc, CurrNode);
            AddNameSpace(NsMgr, 'soap', SoapNS);
            AddNameSpace(NsMgr, 'ns2', XsdNS);
            if FindNodesWithNamespaceManager(CurrNode, '//soap:Body/ns2:getLatestInterestAndExchangeRatesResponse/return/groups/series', NsMgr, CurrNodeList) then
              foreach CurrNode in CurrNodeList do
                with CurrExchRate do
                  begin
                    INIT;
                    if FindNode(CurrNode, 'seriesid', CurrNode2) then
                      "Currency Code" := COPYSTR(CurrNode2.AsXmlElement.InnerText, 4, 3);
                    if FindNode(CurrNode, 'unit', CurrNode2) then
                      Unit := FormatDecmialTextImport(CurrNode2.AsXmlElement.InnerText);
                    "Relational Exch. Rate Amount" := 1;
                    "Relational Adjmt Exch Rate Amt" := 1;
                    if FindNode(CurrNode, 'resultrows', CurrNode2) then
                      begin
                        CurrNode := CurrNode2;
                        if FindNode(CurrNode, 'date', CurrNode2) then
                          "Starting Date" := FormatDateTextImport(CurrNode2.AsXmlElement.InnerText);
                        if FindNode(CurrNode, 'value', CurrNode2) then
                          begin
                            "Exchange Rate Amount" := FormatDecmialTextImport(CurrNode2.AsXmlElement.InnerText);
                            "Exchange Rate Amount" := 1 / "Exchange Rate Amount" * Unit;
                            "Adjustment Exch. Rate Amount" := "Exchange Rate Amount";
                            if not INSERT then
                              MODIFY;
                          end;
                      end;
                  end;
          end;
    end;  

    local procedure HTTPPost(var XmlDoc : XmlDocument;URL : Text[250]) : Boolean;
    var
        MyHTTPClient: HttpClient;
        RequestMessage: HttpRequestMessage;
        ResponseMessage: HttpResponseMessage;
        Headers: HttpHeaders;
        Content: HttpContent;
        CurrNodeList : XmlNodeList;
        XML_text: text;
        InStr: InStream;
        OutStr: OutStream;
    begin
        RequestMessage.SetRequestUri(URL);
        RequestMessage.Method('POST');
        XmlDoc.WriteTo(XML_text); 
        Content.WriteFrom(XML_text);
        Content.GetHeaders(Headers);
        Headers.Remove('Content-Type');
        Headers.Add('Content-Type', 'application/soap+xml;charset=UTF-8;action="urn:getLatestInterestAndExchangeRates"');
        RequestMessage.Content := Content;
        MyHTTPClient.send(RequestMessage, ResponseMessage);
        if not ResponseMessage.IsSuccessStatusCode then
          error(format(ResponseMessage.HttpStatusCode) +' , ' + ResponseMessage.ReasonPhrase);
        if ResponseMessage.IsSuccessStatusCode then
           begin
             ResponseMessage.Content.ReadAs(InStr);
             XMLDOMMgmt.LoadXMLDocumentFromInStream(InStr, XmlDoc);
             XMLDOMMgmt.SetUtf8Declaration(XmlDoc);
           end
         else
           error(format(ResponseMessage.HttpStatusCode));
    end;

    local procedure FormatDecmialTextImport(TextDec : Text[30]) : Decimal;
    var
        TempDec : Decimal;
    begin
        if TextDec = '' then
          exit(0)
        else
          begin
            TempDec := 0.01;
            EVALUATE(TempDec, CONVERTSTR(TextDec, '.', COPYSTR(FORMAT(TempDec),2,1)));
            exit(TempDec);
          end;
    end;

    local procedure FormatDateTextImport(DateText : Text) : Date;
    var
        MyYear : Integer;
        MyMonth : Integer;
        MyDay : Integer;
    begin
        EVALUATE(MyYear, COPYSTR(DateText, 1, 4));
        EVALUATE(MyMonth, COPYSTR(DateText, 6, 2));
        EVALUATE(MyDay, COPYSTR(DateText, 9, 2));
        exit(DMY2DATE(MyDay, MyMonth, MyYear));;
    end;
}
codeunit 57901 "XML DOM Mgt."
{   
    https://diveshboramsdnavblog.wordpress.com/2018/03/09/vs-code-xml-dom-management-part-2/
    trigger OnRun();
    begin
    end;

    procedure AddElement(var pXMLNode: XmlNode; pNodeName: Text; pNodeText: Text; pNameSpace: Text; var pCreatedNode: XmlNode): Boolean
    begin
        IF pNodeText <> '' then
            pCreatedNode := XmlElement.Create(pNodeName, pNameSpace, pNodeText).AsXmlNode
        else
            pCreatedNode := XmlElement.Create(pNodeName, pNameSpace).AsXmlNode;
        Exit(pXMLNode.AsXmlElement.Add(pCreatedNode));
    end;

    procedure AddRootElement(var pXMLDocument: XmlDocument; pNodeName: Text; var pCreatedNode: XmlNode): Boolean
    begin
        pXMLDocument := XmlDocument.Create;
        pCreatedNode := XmlElement.Create(pNodeName).AsXmlNode;
        Exit(pXMLDocument.Add(pCreatedNode));
    end;

    procedure AddRootElementWithPrefix(var pXMLDocument: XmlDocument; pNodeName: Text; pPrefix: Text; pNameSpace: text; var pCreatedNode: XmlNode): Boolean
    begin
        pXMLDocument := XmlDocument.Create;
        pCreatedNode := XmlElement.Create(pNodeName, pNameSpace).AsXmlNode;
        pCreatedNode.AsXmlElement.Add(XmlAttribute.CreateNamespaceDeclaration(pPrefix, pNameSpace));
        Exit(pXMLDocument.Add(pCreatedNode));
    end;

    procedure AddElementWithPrefix(var pXMLNode: XmlNode; pNodeName: Text; pNodeText: Text; pPrefix: Text; pNameSpace: text; var pCreatedNode: XmlNode): Boolean
    begin
        IF pNodeText <> '' then
            pCreatedNode := XmlElement.Create(pNodeName, pNameSpace, pNodeText).AsXmlNode
        else
            pCreatedNode := XmlElement.Create(pNodeName, pNameSpace).AsXmlNode;
        pCreatedNode.AsXmlElement.Add(XmlAttribute.CreateNamespaceDeclaration(pPrefix, pNameSpace));
        exit(pXMLNode.AsXmlElement.Add(pCreatedNode));
    end;

    procedure AddPrefix(var pXMLNode: XmlNode; pPrefix: Text; pNameSpace: text): Boolean
    begin
        pXMLNode.AsXmlElement.Add(XmlAttribute.CreateNamespaceDeclaration(pPrefix, pNameSpace));
        exit(true);
    end;

    procedure AddAttribute(var pXMLNode: XmlNode; pName: Text; pValue: Text): Boolean
    begin
        pXMLNode.AsXmlElement.SetAttribute(pName, pValue);
        exit(true);
    end;

    procedure AddAttributeWithNamespace(var pXMLNode: XmlNode; pName: Text; pNameSpace: text; pValue: Text): Boolean
    begin
        pXMLNode.AsXmlElement.SetAttribute(pName, pNameSpace, pValue);
        exit(true);
    end;

    procedure FindNode(pXMLRootNode: XmlNode; pNodePath: Text; var pFoundXMLNode: XmlNode): Boolean
    begin
        exit(pXMLRootNode.SelectSingleNode(pNodePath, pFoundXMLNode));
    end;

    procedure GetRootNode(pXmlDoc: XmlDocument; var pFoundXMLNode: XmlNode): Boolean
    var
        lXmlElement : XmlElement;
    begin
        pXMLDoc.GetRoot(lXmlElement);
        pFoundXMLNode := lXmlElement.AsXmlNode;
    end;

    procedure FindNodeWithNameSpace(pXMLRootNode: XmlNode; pNodePath: Text; pPrefix: Text; pNamespace: Text; var pFoundXMLNode: XmlNode): Boolean
    var
        lXmlNsMgr: XmlNamespaceManager;
        lXMLDocument: XmlDocument;
    begin

        if pXMLRootNode.IsXmlDocument then
            lXmlNsMgr.NameTable(pXMLRootNode.AsXmlDocument.NameTable)
        else begin
            pXMLRootNode.GetDocument(lXMLDocument);
            lXmlNsMgr.NameTable(lXMLDocument.NameTable);
        end;
        lXMLNsMgr.AddNamespace(pPrefix, pNamespace);
        exit(pXMLRootNode.SelectSingleNode(pNodePath, lXmlNsMgr, pFoundXMLNode));
    end;

    procedure FindNodesWithNameSpace(pXMLRootNode: XmlNode; pXPath: Text; pPrefix: Text; pNamespace: Text; var pFoundXmlNodeList: XmlNodeList): Boolean
    var
        lXmlNsMgr: XmlNamespaceManager;
        lXMLDocument: XmlDocument;
    begin
        if pXMLRootNode.IsXmlDocument then
            lXmlNsMgr.NameTable(pXMLRootNode.AsXmlDocument.NameTable)
        else begin
            pXMLRootNode.GetDocument(lXMLDocument);
            lXmlNsMgr.NameTable(lXMLDocument.NameTable);
        end;
        lXMLNsMgr.AddNamespace(pPrefix, pNamespace);
        exit(FindNodesWithNamespaceManager(pXMLRootNode, pXPath, lXmlNsMgr, pFoundXmlNodeList));
    end;

    procedure FindNodesWithNamespaceManager(pXMLRootNode: XmlNode; pXPath: Text; pXmlNsMgr: XmlNamespaceManager; var pFoundXmlNodeList: XmlNodeList): Boolean
    begin
        IF not pXMLRootNode.SelectNodes(pXPath, pXmlNsMgr, pFoundXmlNodeList) then
            exit(false);
        IF pFoundXmlNodeList.Count = 0 then
            exit(false);
        exit(true);
    end;

    procedure FindNodeXML(pXMLRootNode: XmlNode; pNodePath: Text): Text
    var
        lFoundXMLNode: XmlNode;
    begin
        IF pXMLRootNode.SelectSingleNode(pNodePath, lFoundXMLNode) then
            Exit(lFoundXMLNode.AsXmlElement.InnerXml);
    end;

    procedure FindNodeText(pXMLRootNode: XmlNode; pNodePath: Text): Text
    var
        lFoundXMLNode: XmlNode;
    begin
        IF pXMLRootNode.SelectSingleNode(pNodePath, lFoundXMLNode) then
            Exit(lFoundXMLNode.AsXmlElement.InnerText);
    end;

    procedure FindNodeTextWithNameSpace(pXMLRootNode: XmlNode; pNodePath: Text; pPrefix: Text; pNamespace: Text): Text
    var
        lXmlNsMgr: XmlNamespaceManager;
        lXMLDocument: XmlDocument;
    begin

        if pXMLRootNode.IsXmlDocument then
            lXmlNsMgr.NameTable(pXMLRootNode.AsXmlDocument.NameTable)
        else begin
            pXMLRootNode.GetDocument(lXMLDocument);
            lXmlNsMgr.NameTable(lXMLDocument.NameTable);
        end;
        lXMLNsMgr.AddNamespace(pPrefix, pNamespace);
        Exit(FindNodeTextNs(pXMLRootNode, pNodePath, lXmlNsMgr));
    end;

    procedure FindNodeTextNs(pXMLRootNode: XmlNode; pNodePath: Text; pXmlNsMgr: XmlNamespaceManager): Text
    var
        lFoundXMLNode: XmlNode;
    begin
        IF pXMLRootNode.SelectSingleNode(pNodePath, pXmlNsMgr, lFoundXMLNode) then
            Exit(lFoundXMLNode.AsXmlElement.InnerText);
    end;

    procedure FindNodes(pXMLRootNode: XmlNode; pNodePath: Text; var pFoundXMLNodeList: XmlNodeList): Boolean
    begin
        IF not pXMLRootNode.SelectNodes(pNodePath, pFoundXmlNodeList) then
            exit(false);
        IF pFoundXmlNodeList.Count = 0 then
            exit(false);
        exit(true);
    end;

    procedure FindAttribute(pXMLNode: XmlNode; var pXmlAttribute: XmlAttribute; pAttributeName: Text): Boolean
    begin
        exit(pXMLNode.AsXmlElement.Attributes.Get(pAttributeName, pXmlAttribute));
    end;

    procedure GetAttributeValue(pXMLNode: XmlNode; pAttributeName: Text): Text
    var
        lXmlAttribute: XmlAttribute;
    begin
        If pXMLNode.AsXmlElement.Attributes.Get(pAttributeName, lXmlAttribute) then
            exit(lXmlAttribute.Value);
    end;

    procedure AddDeclaration(var pXMLDocument: XmlDocument; pVersion: Text; pEncoding: Text; pStandalone: Text)
    var
        lXmlDeclaration: XmlDeclaration;
    begin
        lXmlDeclaration := XmlDeclaration.Create(pVersion, pEncoding, pStandalone);
        pXMLDocument.SetDeclaration(lXmlDeclaration);
    end;

    procedure AddGroupNode(var pXMLNode: XmlNode; pNodeName: Text)
    var
        lXMLNewChild: XmlNode;
    begin
        AddElement(pXMLNode, pNodeName, '', '', lXMLNewChild);
        pXMLNode := lXMLNewChild;
    end;

    procedure AddNode(var pXMLNode: XmlNode; pNodeName: Text; pNodeText: Text)
    var
        lXMLNewChild: XmlNode;
    begin
        AddElement(pXMLNode, pNodeName, pNodeText, '', lXMLNewChild);
    end;

    procedure AddLastNode(var pXMLNode: XmlNode; pNodeName: Text; pNodeText: Text)
    var
        lXMLNewChild: XmlNode;
        lXMLElement: XmlElement;
    begin
        AddElement(pXMLNode, pNodeName, pNodeText, '', lXMLNewChild);
        if pXMLNode.GetParent(lXMLElement) then
            pXMLNode := lXMLElement.AsXmlNode;
    end;

    procedure AddNamespaces(var pXmlNsMgr: XmlNamespaceManager; pXMLDocument: XmlDocument)
    var
        lXmlAttributeCollection: XmlAttributeCollection;
        lXmlAttribute: XmlAttribute;
        lXMLElement: XmlElement;
    begin
        pXmlNsMgr.NameTable(pXMLDocument.NameTable);
        pXMLDocument.GetRoot(lXMLElement);
        lXmlAttributeCollection := lXMLElement.Attributes;
        IF lXMLElement.NamespaceUri <> '' then
            pXmlNsMgr.AddNamespace('', lXMLElement.NamespaceUri);
        Foreach lXmlAttribute in lXmlAttributeCollection do
        begin
            if StrPos(lXmlAttribute.Name, 'xmlns:') = 1 then
                pXmlNsMgr.AddNamespace(DELSTR(lXmlAttribute.Name, 1, 6), lXmlAttribute.Value);
        end;
    end;
    
    procedure AddNameSpace(var pXmlNsMgr: XmlNamespaceManager; pPrefix: text; pNamespace: text);
    begin
        pXmlNsMgr.AddNamespace(pPrefix, pNamespace);
    end;

    procedure XMLEscape(pText: Text): Text
    var
        lXMLDocument: XmlDocument;
        lRootXmlNode: XmlNode;
        lXmlNode: XmlNode;
    begin
        lXMLDocument := XmlDocument.Create;
        AddElement(lRootXmlNode, 'XMLEscape', pText, '', lXmlNode);
        exit(lXmlNode.AsXmlElement.InnerXml);
    end;

    procedure LoadXMLDocumentFromText(pXMLText: Text; var pXMLDocument: XmlDocument)
    begin
        IF pXMLText = '' then
            exit;
        XmlDocument.ReadFrom(pXMLText, pXMLDocument);
    end;

    procedure LoadXMLNodeFromText(pXMLText: Text; var pXMLNode: XmlNode)
    var
        lXmlDocument: XmlDocument;
    begin
        LoadXMLDocumentFromText(pXMLText, lXmlDocument);
        pXMLNode := lXmlDocument.AsXmlNode;
    end;

    procedure LoadXMLDocumentFromInStream(pInStream: InStream; var pXMLDocument: XmlDocument)
    begin
        XmlDocument.ReadFrom(pInStream, pXMLDocument);
    end;

    procedure LoadXMLNodeFromInStream(pInStream: InStream; var pXMLNode: XmlNode)
    var
        lXmlDocument: XmlDocument;
    begin
        LoadXMLDocumentFromInStream(pInStream, lXmlDocument);
        pXMLNode := lXmlDocument.AsXmlNode;
    end;

    procedure RemoveNamespaces(pXmlText: Text): Text
    var
        XMLDOMMgt: Codeunit "XML DOM Management";
    begin
        Exit(XMLDOMMgt.RemoveNamespaces(pXmlText));
    end;

    procedure SetUtf8Declaration(var pXMLDocument: XmlDocument);
    var
        Declaration : XmlDeclaration;
    begin
        Declaration := XmlDeclaration.Create('1.0', 'utf-8', 'yes');
        pXMLDocument.SetDeclaration(Declaration);
    end;
}

3 Responses to “AL and SOAP”

  1. WordPress › Error

    There has been a critical error on your website.

    Learn more about debugging in WordPress.