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.
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)
}
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.
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.
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
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.
# ...
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.
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
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
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)
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
}
}