Reputation: 85
Good day Kotlin Developers! I'm building an android app written in Kotlin which calls soap web services api using Retrofit & OkHttp. I'm using this library TikXml for parsing kotlin model classes to Xml & vice-versa. You can take a look at the docs if you want, it might help in understanding my problem.
Here are two examples of the Xml structure of request body for the soap apis that I'm calling:
/tasksbyelementsquery
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:glob="http://sap.com/xi/SAPGlobal20/Global">
<soap:Header/>
<soap:Body>
<!--The Wrapper Class for Data to be passed in the body -->
<glob:SiteLogisticsTaskByElementsQuery_sync>
{data for tasksbyelementsquery}
</glob:SiteLogisticsTaskByElementsQuery_sync>
</soap:Body>
</soap:Envelope>
/tasksbycomponentsquery
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:glob="http://sap.com/xi/SAPGlobal20/Global">
<soap:Header/>
<soap:Body>
<!--The Wrapper Class for Data to be passed in the body -->
<glob:SiteLogisticsTaskByComponentsQuery_sync>
{data for tasksbycomponenetsquery}
</glob:SiteLogisticsTaskByComponentsQuery_sync>
</soap:Body>
</soap:Envelope>
and I have 10 other apis as such with a similar Xml structure. As you can notice that the xml envelope, header & body share the same Xml structure across all apis, as good practices for code reusability
I would like to create a Generic Wrapper Class that generates the envelope, header & body tags for all apis, and only the data structure changes.
So using the Tikxml parsing library, I've generated the following generic class:
GeneralRequestEnvelope.kt
import com.tickaroo.tikxml.annotation.Xml
import com.tickaroo.tikxml.annotation.Element
@Xml(
name = "soap:Envelope",
writeNamespaces = [
"soap=http://schemas.xmlsoap.org/soap/envelope/",
"glob=http://sap.com/xi/SAPGlobal20/Global"
]
)
class GeneralRequestEnvelope <Type> {
@Element(name = "soap:Header")
var header: String? = null
@Element(name = "soap:Body")
var body: Type? = null /**Must ALWAYS be a class but can be any XML defined class**/
}
Then when I call my api from my activity, I specify the Class Type for the body that is supposed to be appended & sent as request body:
//Inside MainActivity
suspend fun callTestSoapApi(
body: TasksByElementsQuery,
onDone: (response: GeneralResponseEnvelope) -> Unit
){
/** Here I'll Define which class type will be used as Body in the General Request*/
val request = GeneralRequestEnvelope<TasksByElementsQuery>()
request.body = body
Timber.e("request after appending body: ${request.body}")
response(onDone){
soapService.callSampleSoapApiAsync(request)
}
}
When I build the project, kapt throws me this build error.
~Project_Location\app\build\tmp\kapt3\stubs\developmentDebug\com\qwerty\soapapitest\codebase\models\envelopes\GeneralRequestEnvelope.java:13: error: The type of field 'body' in class com.qwerty.soapapitest.codebase.models.envelopes.GeneralRequestEnvelope is not a class nor a interface. Only classes or interfaces can be annotated with @Element annotation. If you try to annotate primitives than @PropertyElement private Type body;
The kapt basically expects a class type for the variable body in GeneralRequestEnvelope class
@Element(name = "soap:Body")
var body: Type? = null
So how do I map this generic Type
to some generic class like.. Type::class.java
or something like this? I don't want to create 12 different classes for the same envelope structure for 12 different apis. Any help achieving my usecase using Kotlin Generics will be greatly appreciated.
Upvotes: 0
Views: 742
Reputation: 990
You are misusing Retrofit. Unfortunately due to the biggest and most absurd drawback of Java and its kid Kotlin you are facing the effects of what is called Java Type Erasure. At runtime your types (generics) cannot be resolved. Kotlin has a workaround (not that efficient.... through the reified
parameter of inline functions, but it doesn't help very much in the most complex cases). My suggestion is to separate the XML parser from Retrofit, when you receive/send objects from/to the server, add a field to specify the name of the class (I know...that is not very nice...), so that Retrofit will return the XML serialization of your object with its name, then through Reflection instantiate the object (better use libraries like GSon...or similar). You'll have to pass to the library the CLASS type of your object that you can determine from the name of the object received/sent and the library will instantiate the object for you (it is exaclty what Retrofit and its deserializer/serializer do). I add a small code snippet for you to understand how (it is based on GSon and Retrofit... written in Kotlin):
val netMethod = api.getDataFromSAerver(sessionId, request)//This is the Retrofit method
val response = netMethod.execute()
if (response.isSuccessful) {
try {
val result = response.body()?.string()
if (!result.isNullOrEmpty()) {
val array = JSONObject(result).getJSONArray("objects")
val objects = mutableListOf<Any>()
for (i in 0 until array.length()) {
val objJson = array.get(i)
val s = objJson.toString()
val obj = jsonParser.fromJson(s, resultType.java)//This is the type used for deserialization of your object (as per GSon)
objects.add(obj)
}
return Result(objects)
} else {
return Result(listOf<Unit>(), "no HTTP response", -1)
}
} catch (e: Throwable) {
.... } else {
....
}
The code snipped is based on a JSon response (that I strongly recommend over XML for network traffic... and it is best suited for Restul services/micro-services).
Upvotes: 0