Java Shoppe: Going from Hello World to Docker-Compose
Java Shoppe
Part Zero: Hello Git
Overview
In this assignment, you will set up a Kotlin project, create a simple Hello World program, and initialize a Git repository for your Java Shoppe project. This initial setup will help you to become familiar with Kotlin and to prepare your development environment for the upcoming assignments.
Objectives
By the end of this assignment, you will be able to:
- Set up a Kotlin project and run a simple Hello World program
- Initialize a Git repository and commit your code
Instructions
Kotlin Hello World
- Install the Java Development Kit (JDK) on your machine if you haven't already. You can follow the official installation guide for your operating system: https://www.oracle.com/java/technologies/javase-jdk14-downloads.html.
- Install the Kotlin compiler on your machine by following the official installation guide: https://kotlinlang.org/docs/command-line.html.
- Create a new directory for your Java Shoppe project. You can use the command line or your file explorer to do this. For example, using the command line:
mkdir JavaShoppe
cd JavaShoppe
- Create a new file named
HelloWorld.kt
in your project directory. This file will contain your simple Kotlin Hello World program. - Open the
HelloWorld.kt
file in your favorite text editor or integrated development environment (IDE). Add the following Kotlin code to create a simple Hello World program:
fun main() {
println("Hello, World!")
}
- Compile and run your Kotlin Hello World program using the command line. From the directory containing the
HelloWorld.kt
file, run the following commands:
kotlinc HelloWorld.kt -include-runtime -d HelloWorld.jar
java -jar HelloWorld.jar
You should see the output "Hello, World!" in your terminal.
Git Setup
- Install Git on your machine if you haven't already. You can follow the official installation guide for your operating system: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git.
- Configure your Git username and email by running the following commands in your terminal:
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
Replace "Your Name" and "your.email@example.com" with your actual name and email address.
- Initialize a new Git repository in your Java Shoppe project directory. From the command line, run the following command:
git init
- Add your Kotlin Hello World program to the Git repository by running the following commands:
git add HelloWorld.kt
git commit -m "Initial commit with Hello World program"
You have now successfully set up a Kotlin project with a simple Hello World program and initialized a Git repository for your Java Shoppe project. This initial setup will serve as the foundation for the upcoming assignments.
Part One: Loops
Overview
In this assignment, you will build a simple command line interface (CLI) storefront for a Java Café using Kotlin. The café will have a list of beverages with their names and prices. The user will be able to view the list of available beverages, add them to their order, and checkout.
Objectives
By the end of this assignment, you will be able to:
- Use Kotlin's built-in data structures to store and manipulate data
- Create loops and conditional statements to control program flow
- Implement a basic store interface using loops
Starter Code
import java.util.Scanner
fun main() {
// List of available beverages with their names and prices
val beverages = mapOf(
"Espresso" to 2.99,
"Cappuccino" to 3.49,
"Latte" to 3.99,
"Mocha" to 4.29
)
// Initialize an empty order
val order = mutableMapOf<String, Int>()
// Loop until the user chooses to exit the store
val input = Scanner(System.`in`)
while (true) {
// Print the list of available beverages
println("Welcome to the Java Café!")
println("Available Beverages:")
beverages.forEach { (name, price) ->
println("$name: $$price")
}
// Get the user's choice
print("Enter the name of the beverage you want to add to your order, or type 'checkout' to proceed: ")
val choice = input.nextLine()
// Check if the user wants to checkout
if (choice.lowercase() == "checkout") {
break
}
// Check if the chosen beverage is available in the store
if (beverages.containsKey(choice)) {
// Add the beverage to the order or update its quantity
order[choice] = order.getOrDefault(choice, 0) + 1
println("$choice added to the order.")
} else {
println("Invalid choice. Please try again.")
}
}
// Calculate the total cost
val total = order.entries.sumOf { (item, quantity) -> beverages.getValue(item) * quantity }
// Print the order contents and the total cost
println("\nYour order:")
order.forEach { (item, quantity) ->
println("$item x $quantity")
}
println("Total: $%.2f".format(total))
}
Instructions
- Run the starter code and test the store to make sure you understand how it works.
- Modify the code to display the order contents and total cost before asking for the user's choice.
- Add the ability for the user to remove items from the order.
- Add error handling to prevent the user from inputting invalid choices.
- Modify the code to allow the user to view their order and choose to continue shopping or proceed to checkout.
- Add comments to your code to explain what each part does.
- Test your store thoroughly and make sure it works as expected.
Part 2 - Functions
Overview
In this part of the assignment, you will refactor the Simple CLI Java Café Storefront from Part 1 to use functions. This will make the code more modular, organized, and easier to maintain.
Objectives
By the end of this assignment, you will be able to:
- Use functions to modularize your code and make it more reusable
- Understand the importance of functions in organizing and maintaining code
Instructions
- Review the code from Part 1 of the assignment and identify the major parts of the store that can be converted into functions. Some examples include:
- Displaying available beverages
- Getting user input for adding or removing items from the order
- Updating the order
- Displaying the order contents and total cost
- Create functions for each of the identified parts. Make sure to include comments to describe what each function does and what parameters it takes, if any. For example:
/**
* Display the list of available beverages with their names and prices.
*
* @param beverages A map of beverages and their prices.
*/
fun displayAvailableBeverages(beverages: Map<String, Double>) {
println("Available Beverages:")
beverages.forEach { (name, price) ->
println("$name: $$price")
}
}
- Update the main loop of the store to call the newly created functions instead of having the code directly in the loop. For example:
fun main() {
// ... (previous code)
while (true) {
println("Welcome to the Java Café!")
displayAvailableBeverages(beverages)
val choice = getUserChoice()
// ...
}
// ... (previous code)
}
- Test your refactored store thoroughly and make sure it works as expected.
- Add comments to your code to explain what each part does.
- As an optional exercise, consider adding more features to the store, such as allowing users to update the quantity of items in the order or applying discounts.
- Reflect on how using functions has improved the organization and maintainability of your code.
Part 3 - Classes
Overview
In this part of the assignment, you will refactor the Simple CLI Java Café Storefront from Part 2 to use classes. This will help you to further organize the code and make it more reusable and maintainable.
Objectives
By the end of this assignment, you will be able to:
- Use classes to structure your code and create reusable components
- Understand the benefits of object-oriented programming in organizing and maintaining code
Instructions
- Review the code from Part 2 of the assignment and identify components that can be converted into classes. Consider creating classes for the following components:
- Beverage
- Order
- Café
- Create a
Beverage
class that represents a single beverage with properties such asname
andprice
. Add a method to display the beverage information. For example:
class Beverage(val name: String, val price: Double) {
fun display() {
println("$name: $$price")
}
}
- Create an
Order
class that represents the shopping order. The class should have methods for adding, removing, and displaying items in the order, as well as calculating the total cost. For example:
class Order {
private val items = mutableMapOf<Beverage, Int>()
fun addItem(beverage: Beverage, quantity: Int = 1) {
items[beverage] = items.getOrDefault(beverage, 0) + quantity
}
fun removeItem(beverage: Beverage, quantity: Int = 1) {
if (items.containsKey(beverage)) {
items[beverage] = (items[beverage] ?: 0) - quantity
if (items[beverage] ?: 0 <= 0) {
items.remove(beverage)
}
}
}
fun display() {
items.forEach { (beverage, quantity) ->
println("${beverage.name} x $quantity")
}
}
fun totalCost(): Double {
return items.entries.sumOf { (beverage, quantity) -> beverage.price * quantity }
}
}
- Create a
Cafe
class that represents the Java Café. The class should have a list of available beverages and methods for displaying the beverages, processing user input, and managing the shopping order. For example:
class Cafe(private val beverages: List<Beverage>) {
fun displayBeverages() {
beverages.forEach { it.display() }
}
fun processUserInput(input: String, order: Order) {
// Implement the logic to process user input, add or remove items from the order, and handle errors.
}
}
- Update the main loop of the store to use instances of the
Cafe
,Order
, andBeverage
classes. For example:
fun main() {
// ... (previous code)
val cafe = Cafe(beverageList)
val order = Order()
while (true) {
println("Welcome to the Java Café!")
cafe.displayBeverages()
val choice = getUserChoice()
cafe.processUserInput(choice, order)
// ...
}
// ... (previous code)
}
- Test your refactored store thoroughly and make sure it works as expected.
- Add comments to your code to explain what each part does.
- As an optional exercise, consider adding more features to the store, such as applying discounts or allowing users to update the quantity of items
Part 4 - Gradle
Overview
In this part of the assignment, you will set up Gradle in your Java Shoppe project. Gradle is a build automation tool that allows you to manage dependencies, build, and test your Kotlin projects efficiently. By setting up Gradle, you will create a more organized project structure and streamline your build process.
Objectives
By the end of this assignment, you will be able to:
- Set up a Gradle project for your Kotlin application
- Understand the basics of Gradle build scripts
- Manage your project's dependencies using Gradle
Instructions
Gradle Setup
- Install Gradle on your machine if you haven't already. You can follow the official installation guide for your operating system: https://gradle.org/install/.
- In your Java Shoppe project directory, run the following command to create a new Gradle project:
gradle init --type kotlin-application
This command will create a new Gradle project with the necessary files and directories, including the build.gradle.kts
(Kotlin-based build script) file, and a src/main/kotlin
directory for your Kotlin source files.
- Move your existing Kotlin source files (e.g.,
Beverage.kt
,Order.kt
,Cafe.kt
, andMain.kt
) into thesrc/main/kotlin
directory. - Open the
build.gradle.kts
file in your favorite text editor or integrated development environment (IDE). The file should look similar to this:
plugins {
kotlin("jvm") version "1.5.31"
}
group = "org.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.7.0")
}
tasks {
test {
useJUnitPlatform()
}
}
- Review the contents of the
build.gradle.kts
file. It includes the following sections:plugins
: Specifies the plugins used by the project, including the Kotlin JVM plugin.group
andversion
: Set the group and version of your project.repositories
: Specifies the repositories used to download dependencies, such as Maven Central.dependencies
: Lists your project's dependencies, including the Kotlin standard library and JUnit for testing.tasks
: Defines the tasks for your project, such as thetest
task that uses the JUnit platform.
- To build your project, run the following command in your project directory:
gradle build
This command will compile your Kotlin source files and create a JAR file in the build/libs
Part 5 - SQL
Overview
In this part of the assignment, you will refactor the Simple CLI Java Café Storefront from Part 3 to use an SQLite database for storing beverage information. This will help you understand how to integrate databases into your applications and manage data more efficiently.
Objectives
By the end of this assignment, you will be able to:
- Use SQLite databases to store and manage data in your application
- Understand the benefits of using databases for data persistence and management
Instructions Part 1 - Getting Started
- Install the SQLite library for Kotlin/Java by adding the following dependency to your
build.gradle
file:
dependencies {
// ... (previous dependencies)
implementation("org.xerial:sqlite-jdbc:3.36.0.3")
}
- Import the required modules at the beginning of your code:
import java.sql.Connection
import java.sql.DriverManager
- Create a function to set up the SQLite database and add the initial beverage data. For example:
fun setupDatabase(): Connection {
val conn = DriverManager.getConnection("jdbc:sqlite:beverages.db")
conn.createStatement().use { stmt ->
stmt.execute(
"""
CREATE TABLE IF NOT EXISTS beverages (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL
)
""".trimIndent()
)
// Add initial beverage data if the table is empty
val count = stmt.executeQuery("SELECT COUNT(*) FROM beverages").getInt(1)
if (count == 0) {
val initialBeverages = listOf(
"Small Coffee" to 2.99,
"Medium Coffee" to 3.99,
"Large Coffee" to 4.99,
"Espresso" to 1.99,
)
val preparedStatement = conn.prepareStatement("INSERT INTO beverages (name, price) VALUES (?, ?)")
for ((name, price) in initialBeverages) {
preparedStatement.setString(1, name)
preparedStatement.setDouble(2, price)
preparedStatement.addBatch()
}
preparedStatement.executeBatch()
}
}
return conn
}
- Update the
Cafe
class to use the SQLite database to retrieve the list of available beverages. Modify thedisplayBeverages
method to fetch the beverages from the database and display them. For example:
class Cafe(private val conn: Connection) {
fun displayBeverages() {
val stmt = conn.createStatement()
val resultSet = stmt.executeQuery("SELECT id, name, price FROM beverages")
while (resultSet.next()) {
val id = resultSet.getInt("id")
val name = resultSet.getString("name")
val price = resultSet.getDouble("price")
println("$id. $name: $$price")
}
}
}
- Update the main loop of the store to pass the SQLite connection to the
Cafe
class. For example:
fun main() {
// ... (previous code)
val conn = setupDatabase()
val cafe = Cafe(conn)
val order = Order()
while (true) {
println("Welcome to the Java Café!")
cafe.displayBeverages()
val choice = getUserChoice()
cafe.processUserInput(choice, order)
// ...
}
// ... (previous code)
}
- Update the
processUserInput
method in theCafe
class to fetch the chosen beverage from the database using its ID and add it to the order. For example:
fun processUserInput(input: String, order: Order) {
try {
val beverageId = input.toInt()
val stmt = conn.createStatement()
val resultSet = stmt.executeQuery("SELECT id, name, price FROM beverages WHERE id = $beverageId")
if (resultSet.next()) {
val id = resultSet.getInt("id")
val name = resultSet.getString("name")
val price = resultSet.getDouble("price")
val beverage = Beverage(name, price)
order.addItem(beverage)
println("$name added to the order.")
} else {
println("Invalid choice. Please try again.")
}
} catch (e: NumberFormatException) {
println("Invalid input. Please enter a number.")
}
}
- Test your refactored store thoroughly and make sure it works as expected.
Instructions Part 2 - Transactions
- Make sure you understand Steps 1-6 of the previous instructions before continuing.
- Create a new table called
transactions
to store transaction data. Add the table creation statement to thesetupDatabase
function:
fun setupDatabase(): Connection {
// ... (existing code)
conn.createStatement().use { stmt ->
stmt.execute(
"""
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY,
transaction_date TIMESTAMP NOT NULL,
total_amount REAL NOT NULL
)
""".trimIndent()
)
// ... (existing code)
}
return conn
}
- Create a new table called
transaction_items
to store the details of each transaction item. Add the table creation statement to thesetupDatabase
function:
fun setupDatabase(): Connection {
// ... (existing code)
conn.createStatement().use { stmt ->
stmt.execute(
"""
CREATE TABLE IF NOT EXISTS transaction_items (
id INTEGER PRIMARY KEY,
transaction_id INTEGER NOT NULL,
beverage_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price REAL NOT NULL,
FOREIGN KEY (transaction_id) REFERENCES transactions (id),
FOREIGN KEY (beverage_id) REFERENCES beverages (id)
)
""".trimIndent()
)
// ... (existing code)
}
return conn
}
- Add a
checkout
method to theOrder
class to store the transaction data in thetransactions
andtransaction_items
tables. Make sure to also clear the order after a successful checkout.
class Order {
// ... (existing code)
fun checkout(conn: Connection) {
if (items.isEmpty()) {
println("Your order is empty. No transaction recorded.")
return
}
val transactionTotal = totalCost()
val stmt = conn.prepareStatement("INSERT INTO transactions (transaction_date, total_amount) VALUES (?, ?)")
stmt.setTimestamp(1, java.sql.Timestamp(System.currentTimeMillis()))
stmt.setDouble(2, transactionTotal)
stmt.executeUpdate()
val transactionId = stmt.generatedKeys.getInt(1)
val itemStmt = conn.prepareStatement("INSERT INTO transaction_items (transaction_id, beverage_id, quantity, price) VALUES (?, ?, ?, ?)")
for ((beverage, quantity) in items) {
val bevStmt = conn.prepareStatement("SELECT id FROM beverages WHERE name = ?")
bevStmt.setString(1, beverage.name)
val resultSet = bevStmt.executeQuery()
val beverageId = resultSet.getInt("id")
Part 6 - ORM
Overview
In this part of the assignment, you will refactor the Simple Café from Part 4 to use a Kotlin ORM for database management. You'll learn how to set up a Kotlin project with the Exposed ORM library, define models, and perform CRUD operations using the ORM.
Objectives
By the end of this assignment, you will be able to:
- Set up a Kotlin project with the Exposed ORM library
- Use the Exposed ORM to manage data
- Perform CRUD operations for Beverage and Order models using the Exposed ORM
Instructions
- Set up a new Kotlin project with the necessary dependencies for Exposed ORM. Add the following dependencies to your
build.gradle
file:
dependencies {
implementation("org.jetbrains.exposed:exposed-core:0.35.1")
implementation("org.jetbrains.exposed:exposed-dao:0.35.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.35.1")
implementation("org.xerial:sqlite-jdbc:3.36.0.3")
}
- Define the
Beverage
andOrder
models using Exposed ORM'sTable
class. Create a new file namedModels.kt
with the following content:
import org.jetbrains.exposed.dao.IntIdTable
object Beverages : IntIdTable() {
val name = varchar("name", 255)
val price = decimal("price", 10, 2)
}
object Orders : IntIdTable() {
val orderDate = datetime("order_date")
val totalAmount = decimal("total_amount", 10, 2)
}
object OrderItems : IntIdTable() {
val order = reference("order", Orders)
val beverage = reference("beverage", Beverages)
val quantity = integer("quantity")
val price = decimal("price", 10, 2)
}
- Initialize the database connection and create tables for the models. Add the following code to your
main
function
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
fun main() {
Database.connect("jdbc:sqlite:cafe.db", "org.sqlite.JDBC")
transaction {
SchemaUtils.create(Beverages, Orders, OrderItems)
}
// ... (existing code)
}
- Refactor the
Cafe
class from Part 4 to use the Exposed ORM for CRUD operations. You can remove thesetupDatabase
method, as the Exposed ORM will handle the database connection:
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
class Cafe {
// ... (existing code)
fun displayBeverages() {
transaction {
Beverages.selectAll().forEachIndexed { index, row ->
println("${index + 1}. ${row[Beverages.name]} - $${row[Beverages.price]}")
}
}
}
fun addBeverage(beverage: Beverage) {
transaction {
Beverages.insert {
it[name] = beverage.name
it[price] = beverage.price
}
}
}
fun updateBeverage(beverage: Beverage) {
transaction {
Beverages.update({ Beverages.id eq beverage.id }) {
it[name] = beverage.name
it[price] = beverage.price
}
}
}
fun deleteBeverage(beverageId: Int) {
transaction {
Beverages.deleteWhere { Beverages.id eq beverageId }
}
}
fun processUserInput(choice: Int, order: Order) {
// ... (existing code)
}
}
- Refactor the
Order
class to use the Exposed ORM for checkout:
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction
import java.time.LocalDateTime
class Order {
// ... (existing code)
fun checkout() {
if (items.isEmpty()) {
println("Your order is empty. No transaction recorded.")
return
}
transaction {
val orderId = Orders.insertAndGetId {
it[orderDate] = LocalDateTime.now()
it[totalAmount] = getTotalAmount()
}.value
items.forEach { (beverage, quantity) ->
OrderItems.insert {
it[order] = orderId
it[this.beverage] = beverage.id
it[this.quantity] = quantity
it[price] = beverage.price
}
}
}
println("Order successfully recorded!")
}
}
With these changes, your Kotlin project will now utilize the Exposed ORM to manage data in your SQLite database.
Part 7 - HTTP
Objectives
By the end of this assignment, you will be able to:
- Install and configure the Ktor framework
- Create data classes and routes for the Cafe API
- Implement GET, POST, DELETE, and PUT endpoints
- Test your API endpoints using a tool like Postman or
curl
Setting up Ktor framework
- Add Ktor dependencies to your
build.gradle.kts
file:
dependencies {
implementation("io.ktor:ktor-server-core:1.6.3")
implementation("io.ktor:ktor-server-netty:1.6.3")
implementation("io.ktor:ktor-serialization:1.6.3")
}
-
- Create a
main
function in yourMain.kt
file to start the Ktor server:
- Create a
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main() {
embeddedServer(Netty, port = 8080) {
// Server configuration will go here
}.start(wait = true)
}
Creating Data Classes and Routes
- Create a
Beverage
data class in a new filemodels.kt
:
data class Beverage(val id: Int, val name: String, val price: Double)
- Create a new file
routes.kt
and define your routes for the Cafe API:
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.routing.*
fun Application.registerBeverageRoutes() {
routing {
route("/beverages") {
get {
// GET all beverages
}
post {
// POST a new beverage
}
}
route("/beverages/{id}") {
get {
// GET a specific beverage by id
}
put {
// PUT (update) a specific beverage by id
}
delete {
// DELETE a specific beverage by id
}
}
}
}
Implementing GET, POST, DELETE, and PUT Endpoints
- Update the
registerBeverageRoutes
function inroutes.kt
to implement the GET, POST, DELETE, and PUT endpoints:
import io.ktor.application.*
import io.ktor.features.ContentNegotiation
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.json
fun Application.registerBeverageRoutes() {
install(ContentNegotiation) {
json()
}
routing {
route("/beverages") {
get {
// GET all beverages
}
post {
// POST a new beverage
}
}
route("/beverages/{id}") {
get {
// GET a specific beverage by id
}
put {
// PUT (update) a specific beverage by id
}
delete {
// DELETE a specific beverage by id
}
}
}
}
- Implement the logic for each endpoint using the
Cafe
class. In yourCafeController.kt
file, create functions for handling each of the HTTP methods (GET, POST, DELETE, and PUT) for the cafes and transactions. Use theCafe
andTransaction
classes as your data source, and make sure to handle different request scenarios, such as validation and errors.
For Example:
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/api")
class CafeController {
private val cafeData = CafeData()
@GetMapping("/cafes")
fun getAllCafes(): ResponseEntity<List<Cafe>> {
val cafes = cafeData.getAllCafes()
return ResponseEntity(cafes, HttpStatus.OK)
}
@PostMapping("/cafes")
fun createCafe(@RequestBody cafe: Cafe): ResponseEntity<Cafe> {
if (cafe.name.isBlank() || cafe.price < 0) {
return ResponseEntity(HttpStatus.BAD_REQUEST)
}
val newCafe = cafeData.addCafe(cafe)
return ResponseEntity(newCafe, HttpStatus.CREATED)
}
// Remaining functions for other methods and models go here
}
In this example, the getAllCafes()
function handles GET requests for a list of cafes. The function retrieves all cafes from the CafeData
class and returns the cafe list as an HTTP response with an OK status.
The createCafe()
function handles POST requests to create a new cafe. It takes a Cafe
object as a request body and validates its properties. If the validation fails, it returns a BAD_REQUEST status. Otherwise, it adds the new cafe using the CafeData
class and returns the created cafe with a CREATED status.
Next, implement similar functions for DELETE and PUT methods for both cafes and transactions.
- Update the
CafeData
class:
In your CafeData.kt
file, update the class to handle the CRUD operations for both cafes and transactions. Make sure to handle edge cases such as updating or deleting non-existent items.
For example:
class CafeData {
private val cafes = mutableListOf<Cafe>()
private val transactions = mutableListOf<Transaction>()
// Add the CRUD operations for cafes and transactions here
}
- Test your API endpoints
Start your application if it's not already running:
./gradlew bootRun
Use a tool like Postman or curl
to test your API endpoints. Make sure you can perform GET, POST, DELETE, and PUT requests for both cafes and transactions.
For example, using curl
:
List all cafes (GET):
curl -X GET http://localhost:8080/api/cafes
Create a new cafe (POST):
curl -X POST -H "Content-Type: application/json" -d '{"name": "Sample Cafe", "price": 25.99}' http://localhost:8080/api/cafes
Update a cafe (PUT):
curl -X PUT -H "Content-Type: application/json" -d '{"name": "Updated Cafe", "price": 29.99}' http://localhost:8080/api/cafes/1
Delete a cafe (DELETE):
curl -X DELETE http://localhost:8080/api/cafes/1
Ensure that your API endpoints work as expected, and verify that the changes you make through the API are reflected in your data source.
Part 8 - Docker
Overview
In this assignment, you will learn how to create a Docker container for the Kotlin Cafe API you built in the previous assignments. Docker is a platform that allows you to easily create, deploy, and run applications in containers, which are portable and self-contained units that include all the dependencies required to run the application. By dockerizing your API, you'll be able to run it consistently across different environments, making it easier to share, deploy, and scale.
Objectives
By the end of this assignment, you will be able to:
- Understand the basics of Docker and containerization
- Create a Dockerfile to define your API's container
- Build a Docker image for your API
- Run your API in a Docker container
Instructions
- Install Docker on your machine by following the official installation guide for your operating system: https://docs.docker.com/get-docker/.
- Familiarize yourself with Docker's basic concepts, including containers, images, and the Dockerfile. You can read the Docker's official documentation to get started: https://docs.docker.com/get-started/.
- Create a
Dockerfile
in the root directory of your Kotlin Cafe API project. This file will define the container's configuration, including the base image, dependencies, and the command to run your application. - In the
Dockerfile
, start with a base image that includes the JDK. You can use the official OpenJDK image from Docker Hub. For example, to use OpenJDK 11, add the following line to yourDockerfile
:
FROM openjdk:11
- Set the working directory for your application inside the container. This is where your application's files will be copied and where the commands will be executed. Add the following line to your
Dockerfile
:
WORKDIR /app
- Copy your application's files, including the
build.gradle
andsettings.gradle
, into the container. Add these lines to yourDockerfile
:
COPY build.gradle settings.gradle ./
- Run the
gradle wrapper
command to generate the necessary wrapper files. This will ensure that the correct Gradle version is used inside the container:
./gradlew wrapper
- Copy the
gradle
folder and thegradlew
file into the container. Add these lines to yourDockerfile
:
COPY gradle ./gradle
COPY gradlew ./
- Install the dependencies and build your application using Gradle. Add the following line to your
Dockerfile
:
RUN ./gradlew build
- Copy the rest of your application's files into the container. Add the following line to your
Dockerfile
:
COPY . .
- Expose the port your API is running on so that it can be accessed from outside the container. If your API is running on port 8080, add this line to your
Dockerfile
:
EXPOSE 8080
- Finally, add the command to run your API in the container using the Spring Boot Gradle plugin:
CMD ["./gradlew", "bootRun"]
- Build the Docker image for your API by running the following command in the terminal, from the directory containing your
Dockerfile
:
docker build -t kotlin-cafe-api .
This command will create a Docker image named kotlin-cafe-api
using the configuration defined in your Dockerfile
.
- Run your API in a Docker container by executing the following command:
docker run -p 8080:8080 --name kotlin-cafe-api-container kotlin-cafe-api
This command will start a new container named kotlin-cafe-api-container
using the kotlin-cafe-api
image you built earlier. It will also map port 8080 on your local machine to port 8080 inside the container, allowing you to access your API at http://localhost:8080
.
- Test your API by accessing the endpoints from your web browser or using tools like
curl
or Postman. Ensure that the API works as expected when running inside the Docker container. - (Optional) Learn how to use Docker Compose to manage multi-container applications. Docker Compose allows you to define and run multiple containers as part of a single application, which can be useful if you want to add a database or other services to your project. Read the official documentation to get started: https://docs.docker.com/compose/.
- (Optional) Add a
.dockerignore
file to your project to exclude files and directories that are not needed in the container. This can help reduce the size of your Docker image and speed up the build process. Some common files and directories to exclude include:
.git/
.gitignore
.gradle/
build/
*.log
*.iml
*.class
*.jar
*.war
*.ear
.DS_Store
- (Optional) Share your Dockerized API with others by pushing the image to a container registry like Docker Hub. To do this, first create an account on Docker Hub (https://hub.docker.com/) and follow the official documentation for instructions on how to push your image: https://docs.docker.com/docker-hub/repos/.
Remember to test your API thoroughly to ensure that it works as expected when running inside a Docker container. By completing this assignment, you'll have a portable, self-contained API that can be easily shared, deployed, and scaled using Docker.
Part 9 - Managed SQL
Overview
In this assignment, you will refactor your Java Shoppe API to use a PostgreSQL database instead of an in-memory data store. You will also learn how to use Docker to run your PostgreSQL database, making it easy to deploy and manage your database alongside your Kotlin Ktor API.
Objectives
By the end of this assignment, you will be able to:
- Set up a PostgreSQL database using Docker
- Connect your Kotlin Ktor API to the PostgreSQL database
- Perform CRUD operations on the PostgreSQL database
Instructions
- Pull the official PostgreSQL Docker image by running the following command in the terminal:
docker pull postgres
- Start a new PostgreSQL container using the following command:
docker run --name java-shoppe-postgres -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres
Replace mysecretpassword
with a secure password for your PostgreSQL database.
- Install a PostgreSQL client on your machine, such as pgAdmin or DBeaver, to manage your PostgreSQL database.
- Connect to your PostgreSQL database using the PostgreSQL client. Use
localhost
as the hostname,5432
as the port,postgres
as the user, and the password you set in step 3. - Create a new database named
java_shoppe
- Add the necessary dependencies to your Kotlin Ktor project to work with PostgreSQL. You'll need the JDBC driver for PostgreSQL and Exposed, a Kotlin SQL framework. Add the following dependencies to your
build.gradle.kts
:
implementation("org.postgresql:postgresql:42.3.3")
implementation("org.jetbrains.exposed:exposed-core:0.38.1")
implementation("org.jetbrains.exposed:exposed-dao:0.38.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.38.1")
Remember to sync your project to download and configure the new dependencies.
- Create a
DatabaseConfig.kt
file in your project's source folder to configure your database connection. Use the following code snippet as a starting point:
package com.example.javashoppe
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object DatabaseConfig {
fun init() {
val dbUrl = "jdbc:postgresql://localhost:5432/java_shoppe"
val dbUser = "postgres"
val dbPassword = "mysecretpassword"
Database.connect(dbUrl, driver = "org.postgresql.Driver", user = dbUser, password = dbPassword)
transaction {
// Code to create tables or perform other database operations
}
}
}
Replace mysecretpassword
with the password you set in step 3.
- Update your
Application.kt
file to callDatabaseConfig.init()
when your Ktor application starts:
package com.example.javashoppe
import io.ktor.application.*
import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main() {
embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}
fun Application.module() {
DatabaseConfig.init()
install(StatusPages) {
exception<Throwable> { cause ->
call.respond(HttpStatusCode.InternalServerError, cause.localizedMessage)
}
}
install(Routing) {
// Your existing routes
}
}
- Test your API to ensure it's working correctly with the PostgreSQL database. Use tools like Postman or curl to interact with your API and check if the data is being stored, retrieved, updated, and deleted as expected.
- (Optional) Learn how to use Docker Compose to manage your Kotlin Ktor API and PostgreSQL database as a multi-container application. Docker Compose allows you to define and run multiple containers as part of a single application, which can be useful for managing dependencies and configurations. Read the official documentation to get started: https://docs.docker.com/compose/.
By completing this assignment, you will have refactored your Kotlin Ktor Java Shoppe API to use a PostgreSQL database and learned how to run the database using Docker, providing a more scalable and robust solution for your application.
Part 10 - Docker Compose
Overview
In this assignment, you will refactor your existing Kotlin Ktor Java Shoppe API to use Docker Compose. Docker Compose is a tool for defining and running multi-container Docker applications. By using Docker Compose, you can manage your API and PostgreSQL database as a single unit, making it easier to deploy, scale, and manage your application and its dependencies.
Objectives
By the end of this assignment, you will be able to:
- Understand the basics of Docker Compose
- Create a
docker-compose.yml
file to define your multi-container application - Run your Kotlin Ktor API and PostgreSQL database using Docker Compose
Instructions
- Install Docker Compose on your machine if you haven't already. You can follow the official installation guide for your operating system: https://docs.docker.com/compose/install/.
- Shoppe API project. This file will define the configuration for your multi-container application, including the services, networks, and volumes.
- In the
docker-compose.yml
file, define the two services for your application: the Kotlin Ktor API and the PostgreSQL database. You can use the following example as a starting point:
version: '3.8'
services:
javashoppe-api:
build: .
ports:
- "8080:8080"
depends_on:
- db
environment:
POSTGRES_HOST: db
POSTGRES_PORT: 5432
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: javashoppe
db:
image: "postgres:13"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: javashoppe
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
This configuration defines the javashoppe-api
service, which builds the Docker image for your Kotlin Ktor API using the Dockerfile
in the project's root directory. It also maps port 8080 on your local machine to port 8080 inside the container and depends on the db
service.
The db
service uses the official PostgreSQL 13 image from Docker Hub and sets up the environment variables for the database configuration. It also defines a named volume db-data
to persist the PostgreSQL data.
- Update your
DatabaseConfig.kt
file to read the PostgreSQL configuration from the environment variables:
package com.example.javashoppe
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object DatabaseConfig {
fun init() {
val host = System.getenv("POSTGRES_HOST") ?: "localhost"
val port = System.getenv("POSTGRES_PORT")?.toInt() ?: 5432
val user = System.getenv("POSTGRES_USER") ?: "postgres"
val password = System.getenv("POSTGRES_PASSWORD") ?: "postgres"
val dbName = System.getenv("POSTGRES_DB") ?: "javashoppe"
Database.connect(
url = "jdbc:postgresql://$host:$port/$dbName",
driver = "org.postgresql.Driver",
user = user,
password = password
)
transaction {
SchemaUtils.createMissingTablesAndColumns(Products)
}
}
}
This change allows your application to connect to the PostgreSQL database using the environment variables defined