Android KSoap2 für NAV16

Stand 15.04.2020

Programmierung einer Kotlin-Android App mit Zugriff auf die SOAP-Schnittstelle von Navision 2016 mithilfe der KSoap2 Bibliothek.

Navision Server Einrichten

In der „Dynamics NAV 2016 Administration“ muss der Haken „Enable SOAP Services“ und der Haken „Use NTLM Authentication“ gesetzt sein:

SOAP-Webservice testen

Die URL unter der der Webservice verfügbar ist, erfährt man in Navision im Menü Abteilungen/Verwaltung/IT-Verwaltung/Dienste/Web Services

Um den Webervice zu testen habe ich die Chrome Erweiterung „Wizdler“ verwendet:

https://chrome.google.com/webstore/detail/wizdler/oebpmncolmhiapingjaagmapififiakb

Im Chrome kann man direkt die URL des Webservice aufrufen und hat dann über den „Browse WSDL“ Button rechts neben der Adressleiste die Möglichkeit, die Befehle auszuführen, die der Webservice zur Verfügung stellt.

KSoap2

Um von Android mit der SOAP-Schnittstelle von Navision zu kommunizieren, kann man die ksoap2-Bibliothek verwenden. Die Bibliothek ist hier dokumentiert:

https://simpligility.github.io/ksoap2-android/index.html

Um die Funktionen im Projekt zu verwenden muss man die folgende Zeile in den Bereich „dependencies“ in die build.gradle Datei des App-Moduls hinzufügen:

    implementation 'com.google.code.ksoap2-android:ksoap2-android:3.6.2'

Der Kotlin-Code für den Aufruf eines SOAP-Webservice sieht beispielsweise so aus:

val envelope = SoapSerializationEnvelope(SoapEnvelope.VER11)
envelope.setOutputSoapObject(soapObject)
envelope.dotNet = true

val httpTransportSE = HttpTransportSE(Utils.SOAP_URL)

try {
      httpTransportSE.call(SOAP_ACTION, envelope)

      val soapPrimitive = envelope.response
      result = soapPrimitive.toString()

} catch (e: Exception) {
      e.printStackTrace()
}

Die soapObject-Variable erkläre ich später im Detail. Dort wird der Befehl eingetragen und die Parameter übergeben.

Der Zugriff auf den Navision-Webservice wird damit allerdings noch nicht funktionieren, da der Webservice eine Authentifizierung verlangt.

Authentifizierung per Ntml

Zur Authentifizierung am Webservice kann man „Ntml“ verwenden. Dazu gibt es eine zusätzliche Bibliothek, die ebenfalls in den Bereich „dependencies“ in die build.gradle Datei des App-Moduls hinzugefügt werden muss:

implementation 'com.google.code.ksoap2-android:ksoap2-extra-ntlm:3.6.4'

Die oben verwendete Klasse „HttpTransportSE“ wird durch die Klasse „NtlmTransport“ ersetzt. Zusätzlich müssen dieser Klasse mit der Funktion „setCredentials“ die Anmeldedaten übergeben werden:

val ntlmTransport = NtlmTransport(soapUrl)
ntlmTransport.setCredentials(username, password, domain, "")

try {
      ntlmTransport.call(soapAction, envelope)

      val soapPrimitive = envelope.response
      result = soapPrimitive.toString()

} catch (e: Exception) {
      e.printStackTrace()
}

SOAP-Methoden

Navision stellt für Objekte des Typs „Page“ automatisch einige SOAP-Methoden zur Verfügung. Beispielsweise die „Read“-Methode, die den Schlüsselwert „Code“ als Parameter erwartet.

Dazu wird eine neue Variable des Typs SoapObject erstellt. Der Konstruktor erhält den „Namespace“ und die Methode als Parameter. Anschließend wird mit der Funktion „addProperty“ der Code-Parameter übergeben.

val shippingAgentCode = "DAC"

val soapObject = SoapObject("urn:microsoft-dynamics-schemas/page/integration_shipping_agents", "Read")

soapObject.addProperty("Code", shippingAgentCode)

Das sollte reichen, damit der erste SOAP-Aufruf durchgeführt werden kann.

ReadMultiple Filter

Mit der „ReadMultiple“-Methode können mehrere Datensätze aus Navision ausgelesen werden. Dazu kann man mehrere Filter-Blöcke definieren. Die SOAP-Anfrage sieht dann beispielsweise so aus:

<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
    <Body>
        <ReadMultiple xmlns="urn:microsoft-dynamics-schemas/page/integration_shipping_agents">
            <filter>
                <Field>Code</Field>
                <Criteria>DAC</Criteria>
            </filter>
            <setSize>100</setSize>
        </ReadMultiple>
    </Body>
</Envelope>

Im Code kann man den „filter“-Block hinzufügen indem man dafür neue SoapObject-Variablen erstellt. Mit der „AddProperty“-Funktion werden die Parameter des „filter“-Blocks ausgefüllt. Mit der Funktion „addSoapObject“ kann man den fertigen filter-Block in die Soap-Anfrage hinzufügen.

val soapObject = SoapObject("urn:microsoft-dynamics-schemas/page/integration_shipping_agents", "ReadMultiple")

val soapObjectFilter1 = SoapObject("", "filter")
soapObjectFilter1.addProperty("Field", "Code")
soapObjectFilter1.addProperty("Criteria", "DAC")
soapObject.addSoapObject(soapObjectFilter1)

val soapObjectFilter2 = SoapObject("", "filter")
soapObjectFilter2.addProperty("Field", "Name")
soapObjectFilter2.addProperty("Criteria", "Dachser")
soapObject.addSoapObject(soapObjectFilter2)   

Datumsfilter

Um nach einem Datum zu Filtern muss man wissen, in welchem Format das Datum von Navision akzeptiert wird. Das Datum kann im Format Jahr-Monat-Tag oder Monat, Tag, Jahr (ohne Trennzeichen) übergeben werden.

soapObjectFilter.addProperty("Criteria", "2018-12-20")
soapObjectFilter.addProperty("Criteria", "..12202018")
soapObjectFilter.addProperty("Criteria", "<=12202018")

Der Filter auf das aktuelle Datum kann folgendermaßen programmiert werden:

val simpleDateFormat = SimpleDateFormat("MMddyyyy", Locale.getDefault())
val today: Date = Calendar.getInstance().time
val currentDate = simpleDateFormat.format(today).toString()

val soapObjectFilter = SoapObject("", "filter")
soapObjectFilter.addProperty("Field", "Date")
soapObjectFilter.addProperty("Criteria", "<=$currentDate")
soapObject.addSoapObject(soapObjectFilter)

Antwort XML

anyType{Key=16;IwEAAAJ7/0QAQQBD9;6557125250;; Code=DAC; Name=Dachser; Internet_Address=http://elogistics.dachser.com/shpdl/?l=de_DE&c=18&n=%1; }

bei mehreren Datensätzen im Ergebnis:

anyType{
Integration_Tour_Card=anyType{Key=20;B8FQAAJ7BDAAOAAyADc=9;6129002600;; No=0827; Type=Outbound; Tour_Date=2018-11-26; Status=New; Description=Test Tour SL; Shipping_Agent_Code=WITTY; Shipping_Agent_Service_Code=A-W 7025; Closing_Date=0001-01-01; Shipping_Agent_Name=Werksverkehr; Weight=115.018; Amount=0; Tour_Ending_Date=2018-11-26; Vehicle_ID=W25; };
Integration_Tour_Card=anyType{Key=20;B8FQAAJ7BDAAOAAyADg=9;6129004310;; No=0828; Type=Outbound; Tour_Date=2018-11-26; Status=New; Description=Test Tour SL; Shipping_Agent_Code=WITTY; Shipping_Agent_Service_Code=A-W 7025; Closing_Date=0001-01-01; Shipping_Agent_Name=Werksverkehr; Weight=115.018; Amount=0; Tour_Ending_Date=2018-11-26; Vehicle_ID=W25; };
Integration_Tour_Card=anyType{Key=20;B8FQAAJ7BDAAOAAyADk=9;6129005150;; No=0829; Type=Outbound; Tour_Date=2018-12-01; Status=New; Description=Test Tour SL; Shipping_Agent_Code=DAC; Shipping_Agent_Service_Code=FLEX; Closing_Date=0001-01-01; Shipping_Agent_Name=Dachser; Shipping_Agent_Service_Descr=Trockengut Targoflex; Weight=66.475; Amount=0; Tour_Ending_Date=2018-12-02; Vehicle_ID=1; };

Rückgabe-XML parsen

Direkter Zugriff auf Eigenschaften:

val soapResponseObject: SoapObject = envelope.response as SoapObject

val tourNo = soapResponseObject.getProperty(1).toString()
val tourDate = soapResponseObject.getProperty(3).toString()
val tourStatus = soapResponseObject.getProperty(4).toString()
val tourDescripiton = soapResponseObject.getProperty(5).toString()

Über einen Parser:

http://www.helloandroid.com/tutorials/using-ksoap2-android-and-parsing-output-data

http://seesharpgears.blogspot.com/2010/10/ksoap-android-web-service-tutorial-with.html

class Ksoap2ResultParser {

    /**
     * Parses a single object containing primitive types from the response
     * @param input soap message, one element at a time
     * @param theClass your class object, that contains the same member names and types for the response soap object
     * @return the values parsed
     * @throws NumberFormatException
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */

    fun parseObject(input: String, output: Any) {

        val theClass = output.javaClass
        val fields = theClass.declaredFields

        for (i in 0 until fields.size step 1) {
            val fieldName = fields[i].name
            val xmlTag = "$fieldName="
            val fieldType = fields[i].type
            fields[i].isAccessible = true

            if (input.contains(fieldName)) {

                val strValue = input.substring(input.indexOf(xmlTag) + xmlTag.length, input.indexOf(";", input.indexOf(xmlTag)))

                Log.d(LOG_TAG, "Field $fieldName: $strValue")

                if (strValue.length != 0) {
                    if (fieldType.equals(String::class.java)) {
                        fields[i].set(output, strValue)
                    }

                    if (fieldType.equals(Int::class.java)) {
                        fields[i].setInt(output, strValue.toInt())
                    }

                    if (fieldType.equals(Float::class.java)) {
                        fields[i].setFloat(output, strValue.toFloat())
                    }
                }
            }
        }
    }
}

Codeunit Aufrufe

Beim Aufruf von Codeunits wird der SOAP Namespace nicht komplett kleingeschrieben:

val soapNamespace = "urn:microsoft-dynamics-schemas/codeunit/Witty_Web_Service_Functions"

Dafür werden die Parameter komplett kleingeschrieben.