Skip to content

JVM (Apollo Kotlin)

This is an example of using Delivery Tracker with Apollo Kotlin.

After the initial setup in Steps 1-4, you can easily use the APIs below.

demo.kt
val response = apolloClient.query(
GetTrackLastEventQuery(
carrierId = "kr.cjlogistics",
trackingNumber = "1234567890",
)
).execute()

val notFoundError = response.findDeliveryTrackerError(code="NOT_FOUND")
if (notFoundError != null) {
println("NotFound : ${notFoundError.message}")
} else {
println(response.data?.track?.lastEvent?.status?.code)
}
Notes

The code below is verbose to minimize dependencies. You can simplify your code by using the HTTP Client and JSON Parser libraries you're using.

Step 1. Project settings

Step 1.1. Apply the plugin

Add the Apollo Gradle Plugin (com.apollographql.apollo3) to plugins in your project's build.gradle.kts file.

build.gradle.kts
plugins {
// ...
id("com.apollographql.apollo3") version "3.8.2"
}

The Apollo Plugin includes a compiler that generates models from queries when you build your project.

Step 1.2. Configure the Apollo Gradle plugin

Configure the Apollo Plugin to specify where the Codegen will be, etc. Add the following to your project's build.gradle.kts file.

build.gradle.kts
import java.net.URL
import java.util.Base64
import java.io.IOException

// ...

apollo {
service("DeliveryTracker") {
packageName.set("com.demoproject.deliverytracker.api")
schemaFile.set(File("src/main/graphql/schema.graphqls"))
introspection {
endpointUrl.set("https://apis.tracker.delivery/graphql")
headers.put("Authorization", providers.provider {
var accessToken = providers.gradleProperty("deliveryTracker.introspection.accessToken").orNull
if (accessToken != null) {
return@provider "Bearer $accessToken"
}

val clientId = providers.gradleProperty("deliveryTracker.introspection.clientId").orNull
val clientSecret = providers.gradleProperty("deliveryTracker.introspection.clientSecret").orNull

if (clientId == null || clientSecret == null) {
throw InvalidUserDataException("Either the property 'deliveryTracker.introspection.accessToken' must exist, or both 'deliveryTracker.introspection.clientId' and 'deliveryTracker.introspection.clientSecret' properties must exist.")
}

val connection = URL("https://auth.tracker.delivery/oauth2/token").openConnection() as java.net.HttpURLConnection
connection.requestMethod = "POST"
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
connection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()))
val body = "grant_type=client_credentials".toByteArray()
connection.setRequestProperty("Content-Length", body.size.toString())
connection.outputStream.use { stream ->
stream.write(body, 0, body.size)
}

if (connection.responseCode >= 400) {
val response = connection.errorStream.bufferedReader().use { it.readText() }
throw IOException("Failed to authenticate with Delivery Tracker. Server responded with error: $response")
}

val response = connection.inputStream.bufferedReader().use { it.readText() }
val matchResult = """"access_token"\s*:\s*"(.+?)"""".toRegex().find(response)

accessToken = matchResult?.groups?.get(1)?.value
if (accessToken == null) {
throw IOException("Authentication failed: Unable to parse access token from the response. Response received: $response")
}
"Bearer $accessToken"
})
}
}
}

Step 1.3. Add dependencies

build.gradle.kts
dependencies {
// ...
implementation("com.apollographql.apollo3:apollo-runtime:3.8.2")
}

Step 1.4. Configure the Gradle Properties

Setting up credentials is required for Schema Download.

You will need to modify gradle.properties etc. in your project directory to inject the required Gradle properties.

If you're interested in learning more about how to set up Gradle Properties, you can find more information here.

gradle.properties
# ...
deliveryTracker.introspection.clientId=[YOUR_CLIENT_ID]
deliveryTracker.introspection.clientSecret=[YOUR_CLIENT_SECRET]

If you want to know about clientId, clientSecret, please refer to here.

Step 1.4. Reload Gradle project

Finally, reload the Gradle build script to reflect the changes.

If the schema file does not exist at this time and an error occurs, create a temporary schema file.

echo "type Query { dummy: Boolean }" > src/main/graphql/schema.graphqls

Step 2. Download a schema

Run Gradle's downloadDeliveryTrackerApolloSchemaFromIntrospection task to download the Schema.

./gradlew :downloadDeliveryTrackerApolloSchemaFromIntrospection

Step 3. Write your first query

Write the necessary GraphQL queries for Codegen. The example below is an API to get the last event of a waybill.

src/main/graphql/GetTrackLastEvent.graphql
query GetTrackLastEvent($carrierId: ID!, $trackingNumber: String!) {
track(carrierId: $carrierId, trackingNumber: $trackingNumber) {
lastEvent {
time
status {
code
}
}
}
}

Run Gradle's generateDeliveryTrackerApolloSources Task to CodeGen the API (Query) created above.

./gradlew :generateDeliveryTrackerApolloSources

Step 4. Execute your first query

You can use Delivery Tracker by calling the API as shown in the example below.

Step 4.1. Creating an API Client and Writing Request Logic

src/main/kotlin/com/deomporject/main.kt
package com.demoproject

import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.network.http.DefaultHttpEngine
import com.demoproject.deliverytracker.api.DeliveryTrackerAuthorizationInterceptor
import com.demoproject.deliverytracker.api.GetTrackLastEventQuery
import com.demoproject.deliverytracker.api.findDeliveryTrackerError

suspend fun main() {
val apolloClient = ApolloClient.Builder()
.serverUrl("https://apis.tracker.delivery/graphql")
.httpEngine(DefaultHttpEngine(timeoutMillis = 20_000))
.addHttpInterceptor(
DeliveryTrackerAuthorizationInterceptor(
clientId = "[YOUR_CLIENT_ID]",
clientSecret = "[YOUR_CLIENT_SECRET]"
)
)
.build()

val response = apolloClient.query(
GetTrackLastEventQuery(
carrierId = "kr.cjlogistics",
trackingNumber = "1234567890",
)
).execute()

val notFoundError = response.findDeliveryTrackerError(code="NOT_FOUND")
if (notFoundError != null) {
println("NotFound : ${notFoundError.message}")
} else {
println(response.data?.track?.lastEvent?.status?.code)
}
}

Step 4.2. Write code to handle authorization

src/main/kotlin/com/deomporject/deliverytracker/api/DeliveryTrackerAuthorizationInterceptor.kt
package com.demoproject.deliverytracker.api

import com.apollographql.apollo3.api.http.HttpRequest
import com.apollographql.apollo3.api.http.HttpResponse
import com.apollographql.apollo3.api.json.BufferedSourceJsonReader
import com.apollographql.apollo3.api.json.JsonReader
import com.apollographql.apollo3.network.Http.HttpInterceptor
import com.apollographql.apollo3.network.http.HttpInterceptorChain
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.*
import okio.Buffer
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

class DeliveryTrackerAuthorizationInterceptor(
clientId: String,
clientSecret: String,
private val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build(),
) : HttpInterceptor {
private val credentials = Credentials.basic(clientId, clientSecret)
private val requestBody = FormBody.Builder()
.add("grant_type", "client_credentials")
.build()
private val mutex = Mutex()
private var accessToken: String? = null

private suspend fun fetchNewAccessToken(): String? = suspendCoroutine { continuation ->
val call = okHttpClient.newCall(
Request.Builder()
.url("https://auth.tracker.delivery/oauth2/token")
.addHeader("Authorization", credentials)
.post(requestBody)
.build()
)
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
continuation.resume(null)
}

override fun onResponse(call: Call, response: Response) {
val body = response.body
if (body == null) {
continuation.resume(null)
return
}

val jsonReader = BufferedSourceJsonReader(Buffer().write(body.bytes()))
jsonReader.beginObject()
while (jsonReader.hasNext()) {
if (jsonReader.nextName() != "access_token") {
jsonReader.skipValue()
continue
}
continuation.resume(jsonReader.nextString())
return
}
jsonReader.endObject()
continuation.resume(null)
}
})
}

/**
* Check if errors[*].extensions.code == "UNAUTHENTICATED" exists.
*
* We are using BufferedSourceJsonReader to minimize dependencies.
*/
private fun hasUnauthenticatedError(buffer: Buffer): Boolean {
val jsonReader = BufferedSourceJsonReader(buffer)
jsonReader.beginObject()
while (jsonReader.hasNext()) {
if (jsonReader.nextName() != "errors") {
jsonReader.skipValue()
continue
}
if (jsonReader.peek() == JsonReader.Token.NULL) {
jsonReader.nextNull()
return false
}
jsonReader.beginArray()
while (jsonReader.hasNext()) {
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val name = jsonReader.nextName()
if (name != "extensions" && jsonReader.peek() != JsonReader.Token.BEGIN_OBJECT) {
jsonReader.skipValue()
continue
}
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val extensionName = jsonReader.nextName()
if (extensionName != "code") {
jsonReader.skipValue()
continue
}
if (jsonReader.peek() != JsonReader.Token.STRING) {
jsonReader.skipValue()
continue
}
if (jsonReader.nextString() == "UNAUTHENTICATED") {
return true
}
}
jsonReader.endObject()
}
jsonReader.endObject()
}
jsonReader.endArray()
}
return false
}

private suspend fun getAccessToken(forceFetchNewAccessToken: Boolean = false) = mutex.withLock {
if (accessToken == null || forceFetchNewAccessToken) {
accessToken = fetchNewAccessToken()
}
accessToken
}

override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse {
var token = getAccessToken()

val response = if (token != null) {
chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
} else {
chain.proceed(request)
}
val body = response.body?.readByteArray() ?: return response

if (hasUnauthenticatedError(Buffer().write(body))) {
token = getAccessToken(forceFetchNewAccessToken = true)
if (token != null) {
return chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
}
}

return HttpResponse.Builder(
statusCode=response.statusCode,
)
.headers(response.headers)
.body(Buffer().write(body))
.build()
}
}

Step 4.2. Write an Error Handling Utility (Optional)

src/main/kotlin/com/deomporject/deliverytracker/api/DeliveryTrackerApolloExtensions.kt
package com.demoproject.deliverytracker.api

import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo3.api.Error
import com.apollographql.apollo3.api.Operation

fun <D : Operation.Data> ApolloResponse<D>.findDeliveryTrackerError(code: String): Error? {
return this.errors?.find {
it.extensions?.get("code") == code
}
}