Android Client App for consuming SAP SOAP Web services using ksoap2 library

Recently SAP lauched their SAP Mobile Platform 3.0 (more about that here ) and I decide to write about how one can develop an Android Client App that can consume SAP SOAP Web Services with the ksoap2 library. I am maybe a bit late but it still deserves some attention.

Even though recently I had the chance to try out for myself the SAP Mobile Platform 2.2 and develop some Demo Apps testing both MBO and OData concepts I still personally feel that there are many people out there that are still developing mobile apps for SAP the old-fashion-way: ask for their SAP ABAP developers to prepare a couple of web services and develop mobile clients for these.

One point is that many companies still haven’t updated their Netweaver Gateways, so they can not use the OData goodies that come along with the newer versions. Another reason is that a change of mindset is needed and acceptance of the new web technologies and REST as an architecture.  And lest but not least is that there is still not a stable concept about the offline data management. MBOs were built with keeping that in mind, but SAP has decided not to support it anymore as it was a propriatery “protocol”. The Delta Query support that they mention sounds promising here but I have to try it out in order to judge it 🙂

I have one very old post about consuming SAP web services (or it should be consuming .NET web services that call RFC-enabled Function Modules from an SAP-Backend) where the landscape I had for development was SAP Backend, Visual Studio and SAP .NET connector library. Well, this time I had SAP Backend with enabled Webservices Runtime and as any normal developer would do: I made the use of the web services that you can build with SAP from any Remote-enabled Function module and develop clients that would consume this services. The best library out there that one can make use of at the moment is ksoap2 and since it is free you can use it and adapt it to your needs. I actually came to the idea of this blog post after reading many forums where people complained that they didn’t manage to get it work or they had to change the source and rebuild it after changing the names of the tags or something in the XML bulding logic.

How am I using the ksoap2 library to consume SAP Web services? Very elegant and smooth actually.

1. I developed an wrapper class for the SAPSerializationEnvelope that inherits from the SoapSerializationEnvelope where I just make it create XML-Requests in the SAP manner.

import java.io.IOException;
import org.ksoap2.SoapFault;
import org.ksoap2.serialization.SoapSerializationEnvelope;
import org.xmlpull.v1.XmlSerializer;

public class SAPSerializationEnvelope extends SoapSerializationEnvelope {

public String namespace;
 public SAPSerializationEnvelope(int version, String namespace) {
 super(version);
 this.namespace= namespace;
 }

 @Override
 public void write(XmlSerializer writer) throws IOException {

writer.setPrefix("urn", namespace);
 writer.setPrefix("soapenv", env);
 writer.startTag(env, "Envelope");
 writer.startTag(env, "Header");
 writeHeader(writer);
 writer.endTag(env, "Header");
 writer.startTag(env, "Body");
 writeBody(writer);
 writer.endTag(env, "Body");
 writer.endTag(env, "Envelope");

 }

@Override
 public Object getResponse() throws SoapFault {
 return super.getResponse();
 }

}

2. Made a helper class for making calls.

import java.util.ArrayList;
import java.util.List;
import org.ksoap2.HeaderProperty;
import org.ksoap2.SoapEnvelope;
import org.ksoap2.serialization.SoapObject;
import org.ksoap2.serialization.SoapSerializationEnvelope;
import org.ksoap2.transport.HttpTransportSE;

public class SAPServiceCaller {

String NAMESPACE_SOAP = "http://schemas.xmlsoap.org/soap/envelope/";
 static String NAMESPACE = "urn:sap-com:document:sap:soap:functions:mc-style";
 static String SOAP_ACTION = "";

 public static SoapObject webServiceCall(SoapObject request, String URL, boolean imp_types){
 SoapObject response = null;

 SAPSerializationEnvelope envelope = new SAPSerializationEnvelope(
 SoapEnvelope.VER11, NAMESPACE);
 envelope.implicitTypes = imp_types;
 envelope.dotNet = false;
 envelope.setAddAdornments(false);
 envelope.env = SoapSerializationEnvelope.ENV;

 envelope.setOutputSoapObject(request);

 HttpTransportSE androidHttpTransport = new HttpTransportSE(URL, 30000);
 try {
 List<HeaderProperty> headerList = new ArrayList<HeaderProperty>();
 headerList.add(new HeaderProperty("Authorization", "Basic "
 + org.kobjects.base64.Base64.encode("USER:PASS"
 .getBytes())));
 headerList.add(new HeaderProperty("Connection", "Keep-Alive"));
 androidHttpTransport.debug = true;
 androidHttpTransport.call(SOAP_ACTION, envelope, headerList);
 String answer = androidHttpTransport.responseDump;
 response = (SoapObject) envelope.bodyIn;
 }catch(Exception e){
 e.printStackTrace();
 }

 return response;
 }

}

3. And used it later in calls. A simple call is as follows.

SoapObject request = new SoapObject(
 "urn:sap-com:document:sap:soap:functions:mc-style",
 "ZbapiGetRoutes");
 request.addProperty("RoutesList", "");
SoapObject response;
response = SAPServiceCaller.webServiceCall(request, URL, false);
 SoapObject routesList = (SoapObject) response.getProperty("RoutesList");

This is a simple code that calls a method to return a list of Routes that one has to visit and read the meters in homes and upload them later to the backend.
With this simple “trick” I managed to build a Demo app for an SAP Backend with ISU Component that was displaying the Routes and devices from which the Meter data were read and uploaded to the backend for later billing purposes.

Hope it helps someone out there.

In the following posts I will try to keep up with the trends and write something about what I learned from the course that SAP was offered at openSAP  about Mobile development with the SAP Mobile Platform and maybe other ways how to achieve this as well.

Happy coding!

A little bit of C#… – Extracting correct table structure from a Word document

After a summer break (or  I can say a busy summer) I decided to blog about a problem that I bumped into and I see that is bothering many people but I didn’t manage to find nice implementation.

Recently I had some task to extract data from word documents and well I thought: “Easy one… just use the API that Microsoft has and in all will be done in no-time”. I was so naïve to believe that the API will be without bugs (some of them reported and many unreported ones…).

One of the greatest challenges that I faced was exactly determining the structure of the table in a word document. The table can have horizontally or vertically merged cells and table header row(s). I didn’t pay much attention for the borders and shades since that is artistic thing and not really important for the table structure (when referring the information that the table holds).

So first thing is simple table (no merging, just pure table):

Column 1 Column2 Column 3
Line 1
Line 2
Line 3

Let’s suppose that I have created following classes for representing Cells and Tables

using System;
using System.Collections.Generic;
using Word = Microsoft.Office.Interop.Word;
using Office = Microsoft.Office.Core;
using Microsoft.Office.Tools.Word;
using System.Windows.Forms;
using Microsoft.Office.Interop.Word;
using System.Xml;

public class CustomCell
{
public int cellRow;
public int cellColumn;
public int cellRowSpan;
public int cellColumnSpan;
public String cellText;
}
public class CustomTable
{
public int rowsField;
public int columnsField;
public List cells;
}

In order to get the structure of the given table we simply need to loop through each row and cell and its contents. Simple as 1,2,3… If we have selected a table in Word we can use the following code:


Table wordTable = Application.Selection.Tables[1];
CustomTable table = new CustomTable();
table.cells = new List();
for (int row = 1; row <= wordTable.Rows.Count; row++)
{
for (int column = 1; column <= wordTable.Rows[row].Cells.Count; column++)
{
CustomCell cell = new CustomCell();
cell.cellColumn = column;
cell.cellRow = row;
cell.cellText = wordTable.Cell(row, column).Range.Text;
table.cells.Add(cell);
}
}

This code works also for a table like this:

Merged cell
Line 1
Line 2

Usually the users get creative and want something more in the tables like merged columns or cells for better data representation. In these cases this code is useless…  In case of vertically merged cells this code will still work but it is not the case if there are horizontally merged cells. There is a nice solution to use the Cell.Next method that the Word API offers. In that case the code would look like:


Table wordTable = Application.Selection.Tables[1];
Cell wordCell = wordTable.Cell(1, 1);
CustomTable table = new CustomTable();

table.cells = new List();
while(wordCell!=null)
{
CustomCell cell = new CustomCell();
cell.cellColumn = wordCell.RowIndex;
cell.cellRow = wordCell.ColumnIndex;
cell.cellText = wordCell.Range.Text;
table.cells.Add(cell);
wordCell = wordCell.Next;
}

Here is some a little bit more complex table:

Text Text Text
Text
Text

So when you think that all your problems are solved someone comes and asks for exact row and column span of each cell because maybe they want to have Word documents viewer or just embed the table in a web site. In this case the API doesn’t help with a method or attribute.. you have to find your own algorithm how to get this information. I read and read many ideas and forums: some had broken cells apart and them merged them (?!), then someone was using the height value/weight attribute to decide if the cells have row or column span and many more creative ideas. They all seemed a bit impossible for me because I thought of many exceptions that might happen and the code wouldn’t work properly. So I bumped into a comment saying that the XML structure of the table is a good place to start. So since there is a lot of documentation how to form a legal XML structure for a table I will refer to the following link http://msdn.microsoft.com/en-us/library/office/ff951689.aspx

After you master somewhat the structure you will understand the following code that goes through the cells and gets the information you need:

Table wordTable = Application.Selection.Tables[1];
Cell wordCell = wordTable.Cell(1, 1);
CustomTable table = new CustomTable();
table.cells = new List();

String s = Application.Selection.Tables[1].Range.XML;
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(s);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
nsmgr.AddNamespace("w", "http://schemas.microsoft.com/office/word/2003/wordml");

while (wordCell != null)
{
CustomCell cell = new CustomCell();
cell.cellRow = wordCell.RowIndex;
cell.cellColumn = wordCell.ColumnIndex;
int colspan;
XmlNode exactCell = xmlDoc.SelectNodes("//w:tr[" + wordCell.RowIndex.ToString() + "]/w:tc[" + wordCell.ColumnIndex.ToString() + "]/w:tcPr/w:gridSpan", nsmgr)[0];
if (exactCell != null)
{
colspan = Convert.ToInt16(exactCell.Attributes["w:val"].Value);
}
else
{
colspan = 1;
}

int rowspan = 1;
Boolean endRows = false;
int nextRows = wordCell.RowIndex + 1;
XmlNode exactCellVMerge = xmlDoc.SelectNodes("//w:tr[" + wordCell.RowIndex.ToString() + "]/w:tc[" + wordCell.ColumnIndex.ToString() + "]/w:tcPr/w:vmerge", nsmgr)[0];

if ((exactCellVMerge == null) || (exactCellVMerge != null && exactCellVMerge.Attributes["w:val"] == null))
{
rowspan = 1;
}
else
{
while (nextRows <= wordTable.Rows.Count && !endRows)
{
XmlNode nextCellMerge = xmlDoc.SelectNodes("//w:tr[" + nextRows.ToString() + "]/w:tc[" + wordCell.ColumnIndex.ToString() + "]/w:tcPr/w:vmerge", nsmgr)[0];
if (nextCellMerge != null && (nextCellMerge.Attributes["w:val"] == null))
{
nextRows++;
rowspan++;
continue;
}
else
{
endRows = true;
}
}
}
cell.cellRowSpan = rowspan;
cell.cellColumnSpan = colspan;
cell.cellText = wordCell.Range.Text;
table.cells.Add(cell);
wordCell = wordCell.Next;
}

Last words: Copy, paste, test, use, reuse but don’t abuse 🙂

ABAP, XML, XSLT and something about acquisition of web services in SAP

Long time no written I know… The last couple of months were a bit busy so I guess I owe my blog some more attention… Just noticed that there are maybe three draft posts waiting for approval:  one for the Digra conference that I visited in September (more on: DIGRA2011  and I was a speaker there about the games that I have made for motivating physical activity: Me,  but the folks were even cooler than I could imagine so they really deserve a post), one kind of a review of my new smartphone HTC Desire S (the device: HTC Desire S) and one about ABAP & JSON project that will be the only one that will see the the light of the day as soon as I finish it 😀

I promise this 2012 I will write more often and I guess someone will find what they need in the posts.

As a first 2012 post I  chose to write something about SAP and web service acquisition. This is a really simple example with simple code but it can be further extended and improved in different cases.

I would suppose that the reader is familiar with XML and XSLT (if not please check the following basic tutorials: W3Schools ) and has experience in any programming language and thus will easily “read” and understand the ABAP lines of code. Maybe he/she has also tried some web service acquisition in some other language but is an ABAP beginner and wants to get some job done really quickly.

So.. we have some web-service  from the National Bank of Republic of Macedoniacurrency download – for getting the currency exchange rates on a given date. This is one of the main things that is implemented in any project since any company needs the exchange rates in many of the processes. Since I am not here to write about business processes I will stick to the technical details.

So what one has to do first is to see what are the input parameters and what is the result from the web service. Let’s take the method GetExchangeRates as an example the input parameters are the start and end date for which we need the rates and the result is a string. When the answer is encapsulated in a string it requires some testing to get the full information about the response. I use soapUI to test the methods and analyze the answers and for this method I got something like:

<GetExchangeRateResult><![CDATA[<dsKurs>
  <xs:schema id="dsKurs" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xs:element name="dsKurs" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
      <xs:complexType>
        <xs:choice minOccurs="0" maxOccurs="unbounded">
          <xs:element name="KursZbir">
            <xs:complexType>
              <xs:sequence>
                <xs:element name="RBr" type="xs:double" minOccurs="0" />
                <xs:element name="Datum" type="xs:dateTime" minOccurs="0" />
                <xs:element name="Valuta" type="xs:double" minOccurs="0" />
                <xs:element name="Oznaka" type="xs:string" minOccurs="0" />
                <xs:element name="Drzava" type="xs:string" minOccurs="0" />
                <xs:element name="Nomin" type="xs:double" minOccurs="0" />
                <xs:element name="Sreden" type="xs:double" minOccurs="0" />
                <xs:element name="DrzavaAng" type="xs:string" minOccurs="0" />
                <xs:element name="NazivMak" type="xs:string" minOccurs="0" />
                <xs:element name="NazivAng" type="xs:string" minOccurs="0" />
                <xs:element name="Datum_f" type="xs:dateTime" minOccurs="0" />
              </xs:sequence>
            </xs:complexType>
          </xs:element>
        </xs:choice>
      </xs:complexType>
    </xs:element>
  </xs:schema>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>978</Valuta>
    <Oznaka>EUR</Oznaka>
    <Drzava>ЕМУ</Drzava>
    <Nomin>1</Nomin>
    <Sreden>61.5044</Sreden>
    <DrzavaAng>EMU</DrzavaAng>
    <NazivMak>евро</NazivMak>
    <NazivAng>Euro</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>840</Valuta>
    <Oznaka>USD</Oznaka>
    <Drzava>С А Д</Drzava>
    <Nomin>1</Nomin>
    <Sreden>48.2918</Sreden>
    <DrzavaAng>USA</DrzavaAng>
    <NazivMak>САД долар</NazivMak>
    <NazivAng>US dollar</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>826</Valuta>
    <Oznaka>GBP</Oznaka>
    <Drzava>В.Британија</Drzava>
    <Nomin>1</Nomin>
    <Sreden>74.0526</Sreden>
    <DrzavaAng>U.K.</DrzavaAng>
    <NazivMak>британска фунта</NazivMak>
    <NazivAng>British pound</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>756</Valuta>
    <Oznaka>CHF</Oznaka>
    <Drzava>Швајцарија</Drzava>
    <Nomin>1</Nomin>
    <Sreden>50.7797</Sreden>
    <DrzavaAng>Switzerland</DrzavaAng>
    <NazivMak>швајцарски франк</NazivMak>
    <NazivAng>Swiss franc</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>752</Valuta>
    <Oznaka>SEK</Oznaka>
    <Drzava>Шведска</Drzava>
    <Nomin>1</Nomin>
    <Sreden>6.9485</Sreden>
    <DrzavaAng>Sweden</DrzavaAng>
    <NazivMak>шведска круна</NazivMak>
    <NazivAng>Swedish krona</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>578</Valuta>
    <Oznaka>NOK</Oznaka>
    <Drzava>Норвешка</Drzava>
    <Nomin>1</Nomin>
    <Sreden>8.0115</Sreden>
    <DrzavaAng>Norway</DrzavaAng>
    <NazivMak>норвешка круна</NazivMak>
    <NazivAng>Norwegian krone</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>392</Valuta>
    <Oznaka>JPY</Oznaka>
    <Drzava>Јапонија</Drzava>
    <Nomin>100</Nomin>
    <Sreden>62.8109</Sreden>
    <DrzavaAng>Japan</DrzavaAng>
    <NazivMak>јапонски јен</NazivMak>
    <NazivAng>Japanese yen</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>208</Valuta>
    <Oznaka>DKK</Oznaka>
    <Drzava>Данска</Drzava>
    <Nomin>1</Nomin>
    <Sreden>8.2705</Sreden>
    <DrzavaAng>Denmark</DrzavaAng>
    <NazivMak>данска круна</NazivMak>
    <NazivAng>Danish krone</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>124</Valuta>
    <Oznaka>CAD</Oznaka>
    <Drzava>Канада</Drzava>
    <Nomin>1</Nomin>
    <Sreden>47.5084</Sreden>
    <DrzavaAng>Canada</DrzavaAng>
    <NazivMak>канадски долар</NazivMak>
    <NazivAng>Canadian dollar</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>
  <KursZbir>
    <RBr>7</RBr>
    <Datum>2012-01-13T00:00:00+01:00</Datum>
    <Valuta>36</Valuta>
    <Oznaka>AUD</Oznaka>
    <Drzava>Австралија</Drzava>
    <Nomin>1</Nomin>
    <Sreden>49.9914</Sreden>
    <DrzavaAng>Australia</DrzavaAng>
    <NazivMak>австралиски долар</NazivMak>
    <NazivAng>Australian dollar</NazivAng>
    <Datum_f>2012-01-12T00:00:00+01:00</Datum_f>
  </KursZbir>

Aha there is the definition: KursZbir is the element containing all the data about the exchange rate of the currency according the Macedonian denar. And that is the content we need to put in an ABAP table and do what is necessary with it.

So before you think about which parsing technique should be used and all that I will tell you that the solution is just couple of clicks away. SAP is really sometimes nice to developers and sometimes it is better to search for a day or two first for some technique or a module instead of writing a bunch of code.

Next please go through the following tutorial about how to set up a proxy and test it on the following link. It contains detailed explanation with screens how to create a Proxy class, set up URL and create valid SOAP message and all with couple of clicks no line of code. 🙂 This is not the only one available on the internet you can find many more…

You see here that the method is really simple and the result is just square root of the value sent. The problem is when there is a more complex structure like the one we have to deal with now. Now we have to make a transformation. It is nothing too complicated just a way to tell one really great SAP function how to transform the “weird” XML data string into table data. It is done through the Object Navigator (SE80) . Open the menu: Workbench -> Edit Object. From the dialog select transformation and then create.

For this example the following XSL transformation was used: (i put it here so that you can easily copy and paste )

<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:sap="http://www.sap.com/sapxsl" version="1.0">
  <xsl:strip-space elements="*"/>
  <xsl:template match="/">
    <asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
      <asx:values>
        <ALL_KURS>
          <xsl:apply-templates select="//KursZbir"/>
        </ALL_KURS>
      </asx:values>
    </asx:abap>
  </xsl:template>
  <xsl:template match="KursZbir">
  <ALLKURS>
    <RBR>
      <xsl:value-of select="RBr"/>
    </RBR>
    <DATUM>
      <xsl:value-of select="Datum"/>
    </DATUM>
    <VALUTA>
      <xsl:value-of select="Valuta"/>
    </VALUTA>
    <OZNAKA>
      <xsl:value-of select="Oznaka"/>
    </OZNAKA>
    <DRZAVA>
      <xsl:value-of select="Drzava"/>
    </DRZAVA>
    <NOMIN>
      <xsl:value-of select="Nomin"/>
    </NOMIN>
    <SREDEN>
      <xsl:value-of select="Sreden"/>
    </SREDEN>
    <DRZAVAANG>
      <xsl:value-of select="DrzavaAng"/>
    </DRZAVAANG>
    <NAZIVMAK>
      <xsl:value-of select="NazivMak"/>
    </NAZIVMAK>
    <NAZIVANG>
      <xsl:value-of select="NazivAng"/>
    </NAZIVANG>
    <DATUM_F>
      <xsl:value-of select="Datum_f"/>
    </DATUM_F>
 </ALLKURS>
  </xsl:template>
</xsl:transform>

Actually writing the transformation is the hard part. The ABAP code will actually be short and simple.

Here it goes:

*&---------------------------------------------------------------------*
*& Report  Z_KURS_DOWNLOAD
*&---------------------------------------------------------------------*
 REPORT  z_kurs_download.
 DATA: proxy TYPE REF TO zkurstestco_kurs_soap .
 DATA : st_data TYPE start_date ,
        en_data TYPE end_date.
 data: cr_date type string.
 concatenate sy-datum+6(2) '.' sy-datum+4(2) '.' sy-datum(4) into cr_date.

 DATA: zinput TYPE zkurstestget_exchange_rate_so1,
       zoutput TYPE zkurstestget_exchange_rate_soa.
 DATA: result TYPE string.

 TYPES : BEGIN OF kurszbir,
       rbr TYPE string,
       datum TYPE string,
       valuta TYPE string,
       oznaka TYPE string,
       drzava TYPE string,
       nomin TYPE string,
       sreden TYPE string,
       drzavaang TYPE string,
       nazivmak TYPE string,
       nazivang TYPE string,
       datum_f TYPE string ,
 END OF kurszbir.

 DATA itab TYPE kurszbir OCCURS 0.
 DATA lt_kurs TYPE TABLE OF kurszbir.
 DATA wa_kurs TYPE TABLE OF kurszbir WITH HEADER LINE.

 TRY.
     CREATE OBJECT proxy
       EXPORTING
         logical_port_name = 'ZKURSTESTCO_KURS_SOAP_PORT'.
   CATCH cx_ai_system_fault .
 ENDTRY.

 * ADD SYSTEM DATE IN FORMAT DD.MM.YYYY
 zinput-start_date = cr_date.
 zinput-end_date = cr_date. "cr_date .

 TRY.
     CALL METHOD proxy->get_exchange_rate
       EXPORTING
         input  = zinput
       IMPORTING
         output = zoutput.
   CATCH cx_ai_system_fault .
   CATCH cx_ai_application_fault .
 ENDTRY.

 result = zoutput-GET_EXCHANGE_RATE_RESULT.
 REPLACE ALL OCCURRENCES OF '##' IN result WITH ' ' .

 TRY.
     CALL TRANSFORMATION Z_KURS_XSLT
             SOURCE XML result
             RESULT     all_kurs = lt_kurs.
   CATCH cx_ai_system_fault.
 ENDTRY.

 MOVE lt_kurs TO wa_kurs[]. 

 LOOP AT wa_kurs .
   write: / wa_kurs-datum, ' ', wa_kurs-valuta, ' ',
  wa_kurs-nomin, ' ', wa_kurs-sreden, ' ', wa_kurs-drzavaang, ' ',    wa_kurs-nazivmak, ' '.
 ENDLOOP.

The output of the program is the following:

After that you can either schedule a job or set a transaction and the client can perform it on their own.

I guess someone will find this example useful and the good thing is that it is actually possible to test it because banks are generally reliable for this kind of data 🙂

Next time maybe something more about more complex XML structures.