ABAP Development, NW ABAP Gateway (OData)

Kotlin Application with SAP Gateway Part-2

Create a Kotlin Application

At this point, our SAP Gateway Service is fully developed and tested to support all of our CRUD operations for our custom SAP Table. Now, we can write an application using any programming language of our choice, to call the RESTful web service. Of course, Kotlin is the best, so let’s begin…

Also Read: SAP ABAP 7.5 Certification Preparation Guide

Project Creation

Kotlin was invented by the folks at JetBrains, who gained fame with their excellent development tools. So, naturally, the best IDE for Kotlin is theirs – IntelliJ IDEA.

Gradle

Gradle is the most compatible with Kotlin, so let’s use that for our build tool. Create a new project, as follows:

Select Gradle, Kotlin and Java:

Click Next. Leave GroupId blank, as that is only for a Maven repository. ArtifactId = Project name. Leave Version default:

Click Finish. This will build the following structure:

Double-click to open the “build.gradle” file, and add the following 2 lines to the “dependencies” section:

compile "com.github.kittinunf.fuel:fuel:2.2.1"
compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+"

Add the following to the “repositories” section:

jcenter()

The complete “build.gradle” file should look like the following:

plugins {
    id 'java'
    id 'org.jetbrains.kotlin.jvm' version '1.3.61'
}

version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    testCompile group: 'junit', name: 'junit', version: '4.12'
    compile "com.github.kittinunf.fuel:fuel:2.2.1"
    compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+"
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

Your versions could vary, and the code should still work, since it is backward compatible.

After you add these dependencies, you may see the message in the lower-right:

Go ahead and click “Import Changes”. You should get a successful build message:

This build.gradle file will pull in all of the libraries for you. Go to the menu path “File > Project Structure”, to see them:

Next, go to the “Libraries” section:

Jackson and Fuel

We will be using the Jackson library for all of our JSON parsing needs. If you recall, we can set the SAP Gateway Service to respond and receive data in JSON format.

To more easily manage our HTTP networking requests, we’ll use the Fuel library.

Read Records

At this point, we will walk through creation of the program step-by-step. If you want to skip the details, then just jump to the end and see the entire source code file.

First, lets write the code to read the data from the SAP Gateway, an HTTP GET. Create a new Kotlin class, by right-clicking on the below “Kotlin” node, then “New > Kotlin File/Class”:

Create a file named “TableMaintenance”, which will serve as our only source code file, and the entry point of our program:

Hit Enter to create the new Kotlin file. Just like Java, the main entry point for our Kotlin program will be called “main”. Create the following method:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method main: Entry point for the program...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
fun main() {
    println("Hello World!")
}

Once created, you will see a “Play” icon next to our new method. Click it, to run the program:

The program will compile and run, and you will get the following results:

Now that we know everything is working, let’s create our user menu for our project. Create the following new method, which will display our menu:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method displayMainMenu: Display the user's options and prompt for input...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun displayMainMenu(): Int {
    println()
    println("Task Options: ")
    println("(1) - Display All Data")
    println("(2) - Display a Record")
    println("(3) - Create a Record")
    println("(4) - Update a Record")
    println("(5) - Delete a Record")
    println("(6) - Exit the Program")
    print("Enter an Option Number: ")
    var num = -1
    try {
        num = readLine()!!.toInt()
    } catch (e: NumberFormatException) {
    }
    return num
}

You may recall from our earlier program flow diagram, we want this to be in an infinite loop, until the user chooses to exit with Option 6:

Replace the main() method with the following code:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method main: Entry point for the program...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
fun main() {
    //Process User Selection...
    var answer = 0
    while (answer != 6) {
        answer = displayMainMenu()
        when (answer) {
            1 -> println("Proceeding to Display All Data...")
            2 -> println("Proceeding to Display a Record...")
            3 -> println("Proceeding to Create a Record...")
            4 -> println("Proceeding to Update a Record...")
            5 -> println("Proceeding to Delete a Record...")
            6 -> {
                println("\nThank you for using this amazing program!!...Goodbye...")
                exitProcess(0)
            }
            else -> {
                println()
                println("Invalid Option Number. Please Enter a valid option number 1 thru 6.")
            }
        }
    }
}

Execute the program, and you can test out the user menu. Hit Option 6 to exit:

Now we have our infinite loop, until the user chooses to exit. From our earlier exercises to test the Gateway service, we know there are some strings we will need throughout the program. Create the following constants at the top of the program, above our main method. Be sure to use your own system (MAIN_URL) and auth string (AUTH_STRING), rather than the below:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Global Final Variables used throughout the program (val = Final, var = Variable)...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
const val AUTH_STRING = "amNhcHBblahblahblahblahc="
const val MAIN_URL = "http://<your server>/sap/opu/odata/SAP/ZTEST_KOTLIN_SRV/ZTEST_KOTLINSet"
const val JSON_FORMAT = "?\$format=json"

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method main: Entry point for the program...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
fun main() {

Before we write our next method, let’s talk about the format of the JSON we will receive from the SAP Gateway, and how we may translate that into a Kotlin class…

Data Classes

Kotlin has special classes that we can define only to hold data:

https://kotlinlang.org/docs/reference/data-classes.html

A perfect fit as a container to hold our JSON data nodes. Recall earlier, while testing our SAP Gateway service, we had the following JSON results:

We have the main node of “d”, then a sub node with “results”. Within each repeating record, we have an “__metadata” node. We need to traverse this structure and fetch only the data we want. For example, to parse out the first 2 records, we want the following:

To “Deserialize” a JSON string is to convert it from the above string into a Kotlin object. To “Serialize”, is to go the reverse, from an object back into a string. We’ll need this throughout our program, and we’ll use the Jackson library to help us. We’ll be declaring the following Jackson mapper throughout the program:

val mapper = jacksonObjectMapper()

To represent only the record itself, we’ll define the following Kotlin Data Class:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// A data class which represents a single Customer Memo Record from the ZTEST_KOTLIN SAP table.
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
data class CustomerMemoRecord(
    var Guid: String = "",
    var DateUpdated: String = "",
    var TimeUpdated: String = "",
    var LastUpdatedBy: String = "",
    var OrderNumber: String = "",
    var CustomerNumber: String = "",
    var CustomerMessage: String = ""
)

To mirror our JSON string into an object, to fetch multiple records, we’ll define a Kotlin “List”, which will represent all records:

val customerRecords: List<CustomerMemoRecord> = emptyList()

To mirror the JSON string into a complete set of records, and traverse the “d” and “results” nodes, we’ll define the following 2 Kotlin Data Classes:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Multiple Record GET - RootJsonNodeSet plus CustomerDataSet with all Customer Records...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
data class RootJsonNodeSet(
    @JsonProperty("d")
    val allRecords: CustomerDataSet
)
data class CustomerDataSet(
    @JsonProperty("results")
    val customerRecords: List<CustomerMemoRecord> = emptyList()
)

If we minimize the JSON Nodes, we can correlate more easily with the above Data Class definition:

The above will correspond to the CUSTOMERMEMOSET_GET_ENTITYSET method we coded earlier in our SAP Gateway service.

To fetch a single record, the JSON string looks a little different:

Notice that the “results” node is not in a single record for an HTTP GET. For single records, we’ll define the following Data Class:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Single Record GET for one Customer Memo Record...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
data class RootJsonNodeSingle(
    @JsonProperty("d")
    val singleCustomerRecord: CustomerMemoRecord
)

The above will correspond to the CUSTOMERMEMOSET_GET_ENTITY method we coded earlier in our SAP Gateway service, which fetches a single record.

You may be wondering how we ignore the “__metadata” node. The Jackson Object Mapper has a setting to ignore unknown properties that we don’t have defined in our data class:

//Ignore any properties that don't exist in our CustomerMemoRecord Class...
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

Let’s define one last Data Class, which will represent a GET we will need to do for CUD (Create, Update, Delete) operations in order to fetch the CSRF Token and Cookie:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Data Class for a Fetch record, for CUD Operations...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
data class FetchRecord(
    val CsrfToken: String,
    val Cookie: String,
    var singleCustomerRecord: CustomerMemoRecord
)

Add the 5 above Data Classes at the top of the program, just above the “fun main()” method. You may have noticed that IntelliJ has automatically added the following 2 imports to the top of our program, as we are working:

import com.fasterxml.jackson.annotation.JsonProperty
import kotlin.system.exitProcess

Other various library imports will be added by IntelliJ as we continue to write code.

Date and Time Formats

You may have noticed the strange formats for Date and Time Data Types:

SAP Gateway Definition:

Double-click on Properties, to see the definition:

Edm.DateTime is an Epoch Date, which is a computer time that represents the amount of seconds that have passed since a point in time.

Edm.Time is an XSD:Duration Date Type.

For detailed links with information about these formats, see the Reference section below and the section titled “SAP Gateway Date and Time Format:”.

Add the following 2 methods to the bottom of our program, which will convert these Dates and Times into something readable:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Method jsonDateFormatter: Parse a JSON Date (Epoch Date) into a Java/Kotlin Date
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
private fun jsonDateFormatter(jsonDate: String): String {
    val epochDate = jsonDate.replace("[^0-9]".toRegex(), "")
    val updateDate = Instant.ofEpochMilli(java.lang.Long.parseLong(epochDate))
        .atZone(ZoneId.of("CET"))
        .toLocalDate()
    return updateDate.toString()
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method jsonTimeFormatter: Parse a JSON Time (XSD:Duration Date Type) into Java/Kotlin Time
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun jsonTimeFormatter(jsonTime: String): String {
    val fTime = LocalTime.ofNanoOfDay(Duration.parse(jsonTime).toNanos())
    val df = DateTimeFormatter.ISO_LOCAL_TIME
    return fTime.format(df)
}

The above 2 methods will convert these date and time formats into the following:

Date: YYYY-MM-DD, for example 2020-01-23

Time: HH:MM:SS, for example 18:16:26

Next, let’s write the code to perform an HTTP GET to read one or more records and call our SAP Gateway service. Create the following method to display all records:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method displayDataSet: Display all records in the ZTEST_KOTLIN SAP table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun displayDataSet() {
    println()
    println("...One moment please, retrieving all records...")
    FuelManager.instance.baseHeaders = mapOf("Authorization" to "Basic $AUTH_STRING")
    val mapper = jacksonObjectMapper()
    val url = MAIN_URL + JSON_FORMAT
    //This allows you to parse out only the attributes you'd like, and ignore all others...
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    val (_, _, result) = url.httpGet().responseString()
    when (result) {
        is Result.Failure -> {
            val ex = result.getException()
            print(ex)
        }
        is Result.Success -> {
            val myJsonData: RootJsonNodeSet = mapper.readValue(result.get())
            val memoRecords: List<CustomerMemoRecord> = myJsonData.allRecords.customerRecords
            println("\nTable ZTEST_KOTLIN (${memoRecords.count()} Records):")
            for (x in 0..80) print("=-") // Print 80 times for line separator...
            println()
            println("Guid (key)                           " +
                    "| Date       " +
                    "| Time     " +
                    "| Updated By   " +
                    "| Order #    " +
                    "| Customer # " +
                    "| Memo")
            for (x in 0..80) print("=-") // Print 80 times...
            println()
            memoRecords.forEach {
                println(
                    "${it.Guid} " +
                            "| ${jsonDateFormatter(it.DateUpdated)} " +
                            "| ${jsonTimeFormatter(it.TimeUpdated)} " +
                            "| ${it.LastUpdatedBy.padStart(12)} " + //pad to 12 characters, to line up with column header
                            "| ${it.OrderNumber.padStart(10)} " + //pad to 10 characters
                            "| ${it.CustomerNumber.padStart(10)} " + //pad to 10 characters
                            "| ${it.CustomerMessage}"
                )
            }
            for (x in 0..80) print("=-") // Print 80 times...
            println()
        }
    }
}

Next, we need to update our user menu in the main method, so it calls the above displayDataSet() method:

when (answer) {
    1 -> displayDataSet() //<<<add this

Execute the program (run the main method), and select Option 1 in the user menu:

The above displayed all records currently in the ZTEST_KOTLIN table.

To read a single record, we will require the user to enter the Guid Key for the record. Create the following method, for prompting the user. We will use this method in several places, so we’ll create a generic method that simply prompts the user with a message and returns their entry:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method promptTheUser: Prompt the end user for something...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun promptTheUser(message: String): String {
    print(message)
    return readLine()!!
}

Because we will also need to perform an HTTP GET to retrieve our tokens for the CUD operations, let’s create a generic method that simply fetches a record:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method fetchSingleRecord: Fetch Data for a Single Record...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun fetchSingleRecord(lclUrl: String): FetchRecord {
    val mapper = jacksonObjectMapper()
    FuelManager.instance.baseHeaders = mapOf(
        "Authorization" to "Basic $AUTH_STRING",
        "X-CSRF-Token" to "Fetch"
    )
    //Ignore any properties that don't exist in our CustomerMemoRecord Class...
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    val (_, response, result) = lclUrl.httpGet().responseString()
    val csrfToken: String? = response.headers["x-csrf-token"].elementAt(0)
    val cookie: String? = response.headers["set-cookie"].elementAt(0) + response.headers["set-cookie"].elementAt(1)
    var memoRecord = CustomerMemoRecord()
    when (result) {
        is Result.Failure -> {
            val ex = result.getException()
            print(ex)
        }
        is Result.Success -> {
            val myJsonData: RootJsonNodeSingle = mapper.readValue(result.get())
            memoRecord = myJsonData.singleCustomerRecord
        }
    }
    return FetchRecord(
        CsrfToken = csrfToken.toString(),
        Cookie = cookie.toString(),
        singleCustomerRecord = memoRecord
    )
}

Also, because there will be multiple places we will want to display a single record (for example, after we update it), lets create a generic method which formats a single record:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Method showFormattedRecord: Display of a single record...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun showFormattedRecord(record: CustomerMemoRecord, lclTitle: String) {
    println()
    for (x in 0..50) print("=-") // Print 50 times for line separator...
    println()
    println("$lclTitle Record for Order Number ${record.OrderNumber}:")
    for (x in 0..50) print("=-") // Print 50 times...
    println()
    println("          Guid (key): ${record.Guid}")
    println("   Date Last Updated: ${jsonDateFormatter(record.DateUpdated)}")
    println("   Time Last Updated: ${jsonTimeFormatter(record.TimeUpdated)}")
    println("Last Updated By User: ${record.LastUpdatedBy}")
    println("        Order Number: ${record.OrderNumber}")
    println("     Customer Number: ${record.CustomerNumber}")
    println("       Customer Memo: ${record.CustomerMessage}")
    for (x in 0..50) print("=-") // Print 50 times...
    println()
}

Here is the method which displays a single record:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method displaySingleRecord: Display a single record in the ZTEST_KOTLIN table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun displayRecord(lclGuid: String, lclTitle: String) {
    val singleRecordString = "(Guid=guid'${lclGuid}')"
    val lclUrl = MAIN_URL + singleRecordString + JSON_FORMAT
    val fetchRecord = fetchSingleRecord(lclUrl)
    showFormattedRecord( fetchRecord.singleCustomerRecord, lclTitle )
}

Finally, we need to call our new method from the user menu. Add the following to the main() method for user menu option “2”:

when (answer) {
    1 -> displayDataSet()
    2 -> displayRecord(promptTheUser("Enter a GUID to Display: "), "Current")

In order to get a Guid Key, you can run option 1, first, to display all records, then copy and paste the Guid in order to run option 2 and display the single record. For example, you can run it and do the following:

Highlight and Copy the last Guid (key), then run with option 2:

Now, we’ve completed all of our HTTP GET methods. Let’s move on to the remainder of our methods, which will Create, Update and Delete records…

Create Record

This is the first operation where we will be performing an actual SAP Database update from our Kotlin application. Earlier in the SAP Gateway setup and testing section, we explained how to setup a “GET” operation to fetch our CSRF Token and Cookie for security required when updating the database. For read only operations (i.e. GET), we don’t need these tokens. Above, we created a data class called “FetchRecord” to hold our tokens, and wrote a method called “FetchSingleRecord” which will store our tokens.

A few items we will need for our Create Record method:

  1. Data required from the user to create a new record is:
    • Order Number
    • Customer Number
    • Customer Memo

So we will prompt the user 3 times by calling our generic “promptTheUser” method.

2. Call our Gateway Service for an HTTP GET, with our token “Fetch” option, to get the necessary tokens to update the backend database.

3. Fill the JSON string with the Order Number, Customer Number and Customer Memo, which the user entered in step 1 using our Jackson Object Mapper.

4. Call the SAP Gateway service and do an HTTP POST with our JSON string to create the new record.

5. Display the newly created record to the end-user, along with the new Guid Key and other administrative fields (Date Created, Time Created, etc.).

First, there is a bit of common code we will be using each time we do a Create, Update and Delete. Let’s write the following reusable method, which will initialize the headers for our Fuel HTTP Library:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method initializeFuel: Set the FuelManager for UPDATE or POST...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun initializeFuel(CsrfToken: String, Cookie: String) {
    FuelManager.instance.baseHeaders = mapOf(
        "Authorization" to "Basic $AUTH_STRING",
        "X-CSRF-Token" to CsrfToken,
        "cookie" to Cookie,
        "Content-Type" to "application/json",
        "Accept" to "application/json"
    )
}

Create the following method called “createRecord”:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method createRecord: Create a single record in the ZTEST_KOTLIN table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun createRecord() {
    val lclOrder = promptTheUser("Enter Order Number: ")
    val lclCustomer = promptTheUser("Enter Customer Number: ")
    val lclMemo = promptTheUser("Enter Customer Memo: ")

    //First, fetch the CSRF Token and Cookie, prior to performing the POST...
    var url = MAIN_URL + JSON_FORMAT
    val fetchRecord = fetchSingleRecord(url)
    fetchRecord.singleCustomerRecord.OrderNumber = lclOrder
    fetchRecord.singleCustomerRecord.CustomerNumber = lclCustomer
    fetchRecord.singleCustomerRecord.CustomerMessage = lclMemo
    //Even though we are doing a POST (Create), we still need to fill in all of the
    //attributes, so enter dummy data for these ignored fields...
    fetchRecord.singleCustomerRecord.Guid = "00000000-0000-0000-0000-000000000000"
    fetchRecord.singleCustomerRecord.DateUpdated = """/Date(1578441600000)/"""
    fetchRecord.singleCustomerRecord.LastUpdatedBy = ""
    fetchRecord.singleCustomerRecord.TimeUpdated = "PT13H12M09S"
    val mapper = jacksonObjectMapper()
    // The default mapper, adjusts the field names to lower case camel case, but our
    // Gateway service has upper case (i.e. dateUpdated vs. DateUpdated),
    // so we set the UPPER_CAMEL_CASE property here...
    mapper.propertyNamingStrategy = PropertyNamingStrategy.UPPER_CAMEL_CASE
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    // Serialize the fetchRecord Object back into a JSON String and use for our POST
    // to create the Customer Memo...
    val jsonString = mapper.writeValueAsString(fetchRecord.singleCustomerRecord)
    //Remove the "jsonFormat" URI Option, prior to doing the POST...
    url = MAIN_URL
    val newRecord: CustomerMemoRecord

    initializeFuel(fetchRecord.CsrfToken, fetchRecord.Cookie)
    val postString = """{ "d" : $jsonString }"""

    //This is a synchronous "Blocking Mode" call (i.e. will wait for a response)...
    val (_, _, result) = url.httpPost().body(postString).responseString()
    when (result) {
        is Result.Failure -> {
            println()
            println("Post Failed...")
            println(result.getException().toString() + result.error.response.toString())
        }
        is Result.Success -> {
            val myJsonData: RootJsonNodeSingle = mapper.readValue(result.get())
            newRecord = myJsonData.singleCustomerRecord
            println()
            println("...Customer Memo successfully created...")
            displayRecord(newRecord.Guid, "New")
        }
    }
}

Lastly, add a call to our new “Create” method on our user menu for option 3:

when (answer) {
    1 -> displayDataSet()
    2 -> displayRecord(promptTheUser("Enter a GUID to Display: "), "Current")
    3 -> createRecord()

Run the program, and test the new Create Method:

Update Record

An Update operation is very similar to our Create operation, above, except we will do an “HTTP PUT”, to update an existing record, rather than an HTTP POST. Our Update Record method will do the following:

  1. Prompt the user for a Guid Key for the record to be updated.
  2. Prompt the user for the new Customer Memo to overwrite their existing Memo.
  3. Call our fetchSingleRecord method for the Guid key to be updated. This will retrieve our tokens, plus a JSON string which represents the record.
  4. Modify the above JSON String, and overwrite the memo with our new memo. Utilizing our Jackson Object Mapper, we simply update the CustomerMessage attribute in our CustomerMemoRecord object.
  5. Perform an HTTP PUT with our Fuel HTTP library. Display the old Memo and the new Memo.
  6. Display the newly updated record to the end user.

Create the following updateRecord method:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method updateRecord: Update a single record in the ZTEST_KOTLIN table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun updateRecord() {
    val lclGuid = promptTheUser("Enter a GUID to Update: ")
    val newMemo = promptTheUser("Enter the New Memo: ")
    var url = "$MAIN_URL(Guid=guid'$lclGuid')$JSON_FORMAT"
    val fetchRecord = fetchSingleRecord(url)
    val originalMemo = fetchRecord.singleCustomerRecord.CustomerMessage
    fetchRecord.singleCustomerRecord.CustomerMessage = newMemo
    val mapper = jacksonObjectMapper()
    mapper.propertyNamingStrategy = PropertyNamingStrategy.UPPER_CAMEL_CASE
    val jsonString = mapper.writeValueAsString(fetchRecord.singleCustomerRecord)
    url = "$MAIN_URL(Guid=guid'$lclGuid')"

    initializeFuel(fetchRecord.CsrfToken, fetchRecord.Cookie)
    val putString = """{ "d" : $jsonString }"""
    val (_, _, result) = url.httpPut().body(putString).responseString()
    when (result) {
        is Result.Failure -> {
            println(result.getException().toString())
        }
        is Result.Success -> {
            println("...Customer Memo successfully updated...")
            println("Old Memo: $originalMemo")
            println("New Memo: $newMemo")
            displayRecord(lclGuid, "Updated")
        }
    }
}

Add a call to our new “Update” method on our user menu for option 4:

when (answer) {
    1 -> displayDataSet()
    2 -> displayRecord(promptTheUser("Enter a GUID to Display: "), "Current")
    3 -> createRecord()
    4 -> updateRecord()

To test, we’ll need to get a Guid Key for the record we’d like to update. You can display all records, with option 1, then copy a Guid key to input into the update record option 4, as follows:

Delete Record

The Deletion method is a bit simpler. Since we are simply deleting the record, we only need to provide the Guid Key. We still need to fetch the tokens, just as we did with Create and Update operations.

Our Delete Method will perform the following:

  1. Prompt the user for a Guid Key for the record to be deleted.
  2. Call our fetchSingleRecord method for the Guid key to be deleted and retrieve our tokens.
  3. Perform an HTTP DELETE with our Fuel HTTP library, adding the Guid to the URL.
  4. Display the deleted record to the end user.

Create the following deleteRecord method:

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method deleteRecord: Delete a single record in the ZTEST_KOTLIN table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun deleteRecord() {
    val lclGuid = promptTheUser("Enter a GUID to Delete: ")
    var url = "$MAIN_URL(Guid=guid'$lclGuid')$JSON_FORMAT"
    val fetchRecord = fetchSingleRecord(url)
    val mapper = jacksonObjectMapper()
    mapper.propertyNamingStrategy = PropertyNamingStrategy.UPPER_CAMEL_CASE
    url = "$MAIN_URL(Guid=guid'$lclGuid')"
    initializeFuel(fetchRecord.CsrfToken, fetchRecord.Cookie)
    val (_, _, result) = url.httpDelete().responseString()
    when (result) {
        is Result.Failure -> {
            println(result.getException().toString())
        }
        is Result.Success -> {
            println("...Customer Memo successfully deleted...")
            showFormattedRecord( fetchRecord.singleCustomerRecord, "Deleted" )
        }
    }
}

Add a call to our new “Delete” method on our user menu for option 5:

when (answer) {
    1 -> displayDataSet()
    2 -> displayRecord(promptTheUser("Enter a GUID to Display: "), "Current")
    3 -> createRecord()
    4 -> updateRecord()
    5 -> deleteRecord()

To test, display all records with option 1, decide on which record you want to delete, and copy the Guid key. Run option 5 to delete the record by pasting in the Guid key. You can test as follows:

Complete Kotlin Program

Here is the complete source code for the single TableMaintenance.kt Kotlin file:

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.github.kittinunf.fuel.core.FuelManager
import com.github.kittinunf.fuel.httpDelete
import com.github.kittinunf.fuel.httpGet
import com.github.kittinunf.fuel.httpPost
import com.github.kittinunf.fuel.httpPut
import com.github.kittinunf.result.Result
import java.time.Duration
import java.time.Instant
import java.time.LocalTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.system.exitProcess

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Global Final Variables used throughout the program (val = Final, var = Variable)...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
const val AUTH_STRING = "amNhc<blah><blah><blah>FSUkVMTDc="
const val MAIN_URL = "http://<your server>/sap/opu/odata/SAP/ZTEST_KOTLIN_SRV/ZTEST_KOTLINSet"
const val JSON_FORMAT = "?\$format=json"

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Data Class for a Fetch record, for CUD Operations...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
data class FetchRecord(
    val CsrfToken: String,
    val Cookie: String,
    var singleCustomerRecord: CustomerMemoRecord
)

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// A data class which represents a single Customer Memo Record from the ZTEST_KOTLIN SAP table.
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
data class CustomerMemoRecord(
    var Guid: String = "",
    var DateUpdated: String = "",
    var TimeUpdated: String = "",
    var LastUpdatedBy: String = "",
    var OrderNumber: String = "",
    var CustomerNumber: String = "",
    var CustomerMessage: String = ""
)

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Multiple Record GET - RootJsonNodeSet plus CustomerDataSet with all Customer Records...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
data class RootJsonNodeSet(
    @JsonProperty("d")
    val allRecords: CustomerDataSet
)
data class CustomerDataSet(
    @JsonProperty("results")
    val customerRecords: List<CustomerMemoRecord> = emptyList()
)

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Single Record GET for one Customer Memo Record...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
data class RootJsonNodeSingle(
    @JsonProperty("d")
    val singleCustomerRecord: CustomerMemoRecord
)

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method main: Entry point for the program...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
fun main() {
    //Process User Selection...
    var answer = 0
    while (answer != 6) {
        answer = displayMainMenu()
        when (answer) {
            1 -> displayDataSet()
            2 -> displayRecord(promptTheUser("Enter a GUID to Display: "), "Current")
            3 -> createRecord()
            4 -> updateRecord()
            5 -> deleteRecord()
            6 -> {
                println("\nThank you for using this amazing program!!...Goodbye...")
                exitProcess(0)
            }
            else -> {
                println()
                println("Invalid Option Number. Please Enter a valid option number 1 thru 6.")
            }
        }
    }
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method promptTheUser: Prompt the end user for something...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun promptTheUser(message: String): String {
    print(message)
    return readLine()!!
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method displayMainMenu: Display the user's options and prompt for input...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun displayMainMenu(): Int {
    println()
    println("Task Options: ")
    println("(1) - Display All Data")
    println("(2) - Display a Record")
    println("(3) - Create a Record")
    println("(4) - Update a Record")
    println("(5) - Delete a Record")
    println("(6) - Exit the Program")
    print("Enter an Option Number: ")
    var num = -1
    try {
        num = readLine()!!.toInt()
    } catch (e: NumberFormatException) {
    }
    return num
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Method displaySingleRecord: Display a single record in the ZTEST_KOTLIN table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun displayRecord(lclGuid: String, lclTitle: String) {
    val singleRecordString = "(Guid=guid'${lclGuid}')"
    val lclUrl = MAIN_URL + singleRecordString + JSON_FORMAT
    val fetchRecord = fetchSingleRecord(lclUrl)
    showFormattedRecord( fetchRecord.singleCustomerRecord, lclTitle )
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Method showFormattedRecord: Display of a single record...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun showFormattedRecord(record: CustomerMemoRecord, lclTitle: String) {
    println()
    for (x in 0..50) print("=-") // Print 50 times for line separator...
    println()
    println("$lclTitle Record for Order Number ${record.OrderNumber}:")
    for (x in 0..50) print("=-") // Print 50 times...
    println()
    println("          Guid (key): ${record.Guid}")
    println("   Date Last Updated: ${jsonDateFormatter(record.DateUpdated)}")
    println("   Time Last Updated: ${jsonTimeFormatter(record.TimeUpdated)}")
    println("Last Updated By User: ${record.LastUpdatedBy}")
    println("        Order Number: ${record.OrderNumber}")
    println("     Customer Number: ${record.CustomerNumber}")
    println("       Customer Memo: ${record.CustomerMessage}")
    for (x in 0..50) print("=-") // Print 50 times...
    println()
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method fetchSingleRecord: Fetch Data for a Single Record...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun fetchSingleRecord(lclUrl: String): FetchRecord {
    val mapper = jacksonObjectMapper()
    FuelManager.instance.baseHeaders = mapOf(
        "Authorization" to "Basic $AUTH_STRING",
        "X-CSRF-Token" to "Fetch"
    )
    //Ignore any properties that don't exist in our CustomerMemoRecord Class...
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    val (_, response, result) = lclUrl.httpGet().responseString()
    val csrfToken: String? = response.headers["x-csrf-token"].elementAt(0)
    val cookie: String? = response.headers["set-cookie"].elementAt(0) + response.headers["set-cookie"].elementAt(1)
    var memoRecord = CustomerMemoRecord()
    when (result) {
        is Result.Failure -> {
            val ex = result.getException()
            print(ex)
        }
        is Result.Success -> {
            val myJsonData: RootJsonNodeSingle = mapper.readValue(result.get())
            memoRecord = myJsonData.singleCustomerRecord
        }
    }
    return FetchRecord(
        CsrfToken = csrfToken.toString(),
        Cookie = cookie.toString(),
        singleCustomerRecord = memoRecord
    )
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method displayDataSet: Display all records in the ZTEST_KOTLIN SAP table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun displayDataSet() {
    println()
    println("...One moment please, retrieving all records...")
    FuelManager.instance.baseHeaders = mapOf("Authorization" to "Basic $AUTH_STRING")
    val mapper = jacksonObjectMapper()
    val url = MAIN_URL + JSON_FORMAT
    //This allows you to parse out only the attributes you'd like, and ignore all others...
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    val (_, _, result) = url.httpGet().responseString()
    when (result) {
        is Result.Failure -> {
            val ex = result.getException()
            print(ex)
        }
        is Result.Success -> {
            val myJsonData: RootJsonNodeSet = mapper.readValue(result.get())
            val memoRecords: List<CustomerMemoRecord> = myJsonData.allRecords.customerRecords
            println("\nTable ZTEST_KOTLIN (${memoRecords.count()} Records):")
            for (x in 0..80) print("=-") // Print 80 times for line separator...
            println()
            println("Guid (key)                           " +
                    "| Date       " +
                    "| Time     " +
                    "| Updated By   " +
                    "| Order #    " +
                    "| Customer # " +
                    "| Memo")
            for (x in 0..80) print("=-") // Print 80 times...
            println()
            memoRecords.forEach {
                println(
                    "${it.Guid} " +
                            "| ${jsonDateFormatter(it.DateUpdated)} " +
                            "| ${jsonTimeFormatter(it.TimeUpdated)} " +
                            "| ${it.LastUpdatedBy.padStart(12)} " + //pad to 12 characters, to line up with column header
                            "| ${it.OrderNumber.padStart(10)} " + //pad to 10 characters
                            "| ${it.CustomerNumber.padStart(10)} " + //pad to 10 characters
                            "| ${it.CustomerMessage}"
                )
            }
            for (x in 0..80) print("=-") // Print 80 times...
            println()
        }
    }
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method createRecord: Create a single record in the ZTEST_KOTLIN table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun createRecord() {
    val lclOrder = promptTheUser("Enter Order Number: ")
    val lclCustomer = promptTheUser("Enter Customer Number: ")
    val lclMemo = promptTheUser("Enter Customer Memo: ")

    //First, fetch the CSRF Token and Cookie, prior to performing the POST...
    var url = MAIN_URL + JSON_FORMAT
    val fetchRecord = fetchSingleRecord(url)
    fetchRecord.singleCustomerRecord.OrderNumber = lclOrder
    fetchRecord.singleCustomerRecord.CustomerNumber = lclCustomer
    fetchRecord.singleCustomerRecord.CustomerMessage = lclMemo
    //Even though we are doing a POST (Create), we still need to fill in all of the
    //attributes, so enter dummy data for these ignored fields...
    fetchRecord.singleCustomerRecord.Guid = "00000000-0000-0000-0000-000000000000"
    fetchRecord.singleCustomerRecord.DateUpdated = """/Date(1578441600000)/"""
    fetchRecord.singleCustomerRecord.LastUpdatedBy = ""
    fetchRecord.singleCustomerRecord.TimeUpdated = "PT13H12M09S"
    val mapper = jacksonObjectMapper()
    // The default mapper, adjusts the field names to lower case camel case, but our
    // Gateway service has upper case (i.e. dateUpdated vs. DateUpdated),
    // so we set the UPPER_CAMEL_CASE property here...
    mapper.propertyNamingStrategy = PropertyNamingStrategy.UPPER_CAMEL_CASE
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    // Serialize the fetchRecord Object back into a JSON String and use for our POST
    // to create the Customer Memo...
    val jsonString = mapper.writeValueAsString(fetchRecord.singleCustomerRecord)
    //Remove the "jsonFormat" URI Option, prior to doing the POST...
    url = MAIN_URL
    val newRecord: CustomerMemoRecord

    initializeFuel(fetchRecord.CsrfToken, fetchRecord.Cookie)
    val postString = """{ "d" : $jsonString }"""

    //This is a synchronous "Blocking Mode" call (i.e. will wait for a response)...
    val (_, _, result) = url.httpPost().body(postString).responseString()
    when (result) {
        is Result.Failure -> {
            println()
            println("Post Failed...")
            println(result.getException().toString() + result.error.response.toString())
        }
        is Result.Success -> {
            val myJsonData: RootJsonNodeSingle = mapper.readValue(result.get())
            newRecord = myJsonData.singleCustomerRecord
            println()
            println("...Customer Memo successfully created...")
            displayRecord(newRecord.Guid, "New")
        }
    }
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method updateRecord: Update a single record in the ZTEST_KOTLIN table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun updateRecord() {
    val lclGuid = promptTheUser("Enter a GUID to Update: ")
    val newMemo = promptTheUser("Enter the New Memo: ")
    var url = "$MAIN_URL(Guid=guid'$lclGuid')$JSON_FORMAT"
    val fetchRecord = fetchSingleRecord(url)
    val originalMemo = fetchRecord.singleCustomerRecord.CustomerMessage
    fetchRecord.singleCustomerRecord.CustomerMessage = newMemo
    val mapper = jacksonObjectMapper()
    mapper.propertyNamingStrategy = PropertyNamingStrategy.UPPER_CAMEL_CASE
    val jsonString = mapper.writeValueAsString(fetchRecord.singleCustomerRecord)
    url = "$MAIN_URL(Guid=guid'$lclGuid')"

    initializeFuel(fetchRecord.CsrfToken, fetchRecord.Cookie)
    val putString = """{ "d" : $jsonString }"""
    val (_, _, result) = url.httpPut().body(putString).responseString()
    when (result) {
        is Result.Failure -> {
            println(result.getException().toString())
        }
        is Result.Success -> {
            println("...Customer Memo successfully updated...")
            println("Old Memo: $originalMemo")
            println("New Memo: $newMemo")
            displayRecord(lclGuid, "Updated")
        }
    }
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method deleteRecord: Delete a single record in the ZTEST_KOTLIN table...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
private fun deleteRecord() {
    val lclGuid = promptTheUser("Enter a GUID to Delete: ")
    var url = "$MAIN_URL(Guid=guid'$lclGuid')$JSON_FORMAT"
    val fetchRecord = fetchSingleRecord(url)
    val mapper = jacksonObjectMapper()
    mapper.propertyNamingStrategy = PropertyNamingStrategy.UPPER_CAMEL_CASE
    url = "$MAIN_URL(Guid=guid'$lclGuid')"
    initializeFuel(fetchRecord.CsrfToken, fetchRecord.Cookie)
    val (_, _, result) = url.httpDelete().responseString()
    when (result) {
        is Result.Failure -> {
            println(result.getException().toString())
        }
        is Result.Success -> {
            println("...Customer Memo successfully deleted...")
            showFormattedRecord( fetchRecord.singleCustomerRecord, "Deleted" )
        }
    }
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method initializeFuel: Set the FuelManager for UPDATE or POST...
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun initializeFuel(CsrfToken: String, Cookie: String) {
    FuelManager.instance.baseHeaders = mapOf(
        "Authorization" to "Basic $AUTH_STRING",
        "X-CSRF-Token" to CsrfToken,
        "cookie" to Cookie,
        "Content-Type" to "application/json",
        "Accept" to "application/json"
    )
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method jsonDateFormatter: Parse a JSON Date (Epoch Date) into a Java/Kotlin Date
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun jsonDateFormatter(jsonDate: String): String {
    val epochDate = jsonDate.replace("[^0-9]".toRegex(), "")
    val updateDate = Instant.ofEpochMilli(java.lang.Long.parseLong(epochDate))
        .atZone(ZoneId.of("CET"))
        .toLocalDate()
    return updateDate.toString()
}

//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Method jsonTimeFormatter: Parse a JSON Time (XSD:Duration Date Type) into Java/Kotlin Time
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
private fun jsonTimeFormatter(jsonTime: String): String {
    val fTime = LocalTime.ofNanoOfDay(Duration.parse(jsonTime).toNanos())
    val df = DateTimeFormatter.ISO_LOCAL_TIME
    return fTime.format(df)
}

Leave a Reply

Your email address will not be published. Required fields are marked *