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.