delete Java posts (refs #8)
Build & Push Hugo Site Image / Build & Push Image (push) Successful in 14s
Build & Push Hugo Site Image / Build & Push Image (push) Successful in 14s
This commit is contained in:
@@ -1,179 +0,0 @@
|
||||
+++
|
||||
categories = ["software"]
|
||||
tags = ["java"]
|
||||
date = 2024-11-10T15:55:42Z
|
||||
description = ""
|
||||
draft = false
|
||||
slug = "java-spring-tomcat"
|
||||
title = "☕ Developing Spring REST API - Pt. I"
|
||||
author = "nicholas"
|
||||
+++
|
||||
|
||||
Much time has passed since I last worked with Spring framework in Java. I need to build out a development environment to enable me to build Java applications. In this log, the environment will be configured to include the following tools and features:
|
||||
* Eclipse IDE
|
||||
* Java (JRE, JDK)
|
||||
* Maven (dependency manager, etc.)
|
||||
* Spring Boot (Java framework)
|
||||
|
||||
This log is a series of baby steps that will result in basic implementation of a REST API.
|
||||
|
||||
|
||||
## Code Editor
|
||||
First, I need a code editor/IDE. I selected Eclipse for no particular reason. My understanding is that IntelliJ IDEA is a more modern, refined alternative that I may try down the line. For now, all I need is a default installation to get started.
|
||||
|
||||
|
||||
## Hello World
|
||||
I allow the IDE to walk me through a hello world tutorial to become familiar with the software and remind myself of the basic structure of a Java program.
|
||||
|
||||
|
||||
## Spring REST API endpoint
|
||||
My next milestone is to set up a trivial API endpoint to familiarize myself with Spring MVC. I will use this framework to build an endpoint that will return "hello world". Eventually, this project will become an API for a plant simulation app.
|
||||
|
||||
* I use spring initializr to automate the creation of the necessary boilerplate to run a minimally viable endpoint.
|
||||
|
||||
* I create a controller class for the API: PlantController.java.
|
||||
* This class uses Spring's @RestController and @GetMapping annotations to define the controller and one endpoint.
|
||||
* For now, I have defined a GET endpoint at /helloworld, which returns the string "hello world".
|
||||
|
||||
```java
|
||||
package plantsim.PlantSimulatorAPI;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class PlantController {
|
||||
|
||||
@GetMapping("/helloworld")
|
||||
public String helloworld() {
|
||||
return "hello world";
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
It works. Now I can configure my data models and database.
|
||||
|
||||
## Database
|
||||
Database configuration will require a few steps.
|
||||
|
||||
* Create in-memory database with h2
|
||||
* Using the credentials defined below and in application.properties I am able to connect to the database testdb at /h2-console.
|
||||
|
||||
```java
|
||||
spring.datasource.url=jdbc:h2:mem:testdb
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=password
|
||||
spring.h2.console.enabled=true
|
||||
```
|
||||
|
||||
* In the h2 console, I can execute SQL queries and explore the database structure, etc.
|
||||
|
||||
1. Initialize database – populate with data
|
||||
|
||||
* Since I have defined an entity class for the plant object, Hibernate will automatically create the plant database table for me. I will need to populate the table data, however.
|
||||
* I populate the tale with data using data.sql. By default, Spring Boot will look in src/main/resources for the script and execute it after the table has been initialized (according to application.properties -> spring.jpa.defer-datasource-initialization=true
|
||||
* I configure the script below to insert a couple of rows:
|
||||
|
||||
```sql
|
||||
-- Insert rows
|
||||
INSERT INTO plant (name) VALUES ('Ficus Audrey');
|
||||
INSERT INTO plant (name) VALUES ('Lucky Bamboo');
|
||||
INSERT INTO plant (name) VALUES ('Chinese Money Plant');
|
||||
```
|
||||
3. Configure API endpoint to return plant objects
|
||||
|
||||
* To return a plant object instead of a string, I will need to configure following:
|
||||
* Plant entity that matches database schema – This will use object-relational mapping (ORM) to map my Java entities to my database table plant
|
||||
|
||||
```java
|
||||
package plantsim.PlantSimulatorAPI;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
public class Plant {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
public Plant() {
|
||||
}
|
||||
|
||||
public Plant(Long id, String name) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
* Add JPA dependency to pom.xml – JPersistence API
|
||||
```
|
||||
jakarta.persistence
|
||||
jakarta.persistence-api
|
||||
3.1.0
|
||||
```
|
||||
* Create a JPA repository to fetch the Plant objects from testdb.
|
||||
|
||||
|
||||
|
||||
|
||||
```java
|
||||
package plantsim.PlantSimulatorAPI;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PlantRepository extends JpaRepository {
|
||||
}
|
||||
```
|
||||
|
||||
* Update the controller to return the Plant object.
|
||||
|
||||
|
||||
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private PlantRepository plantRepository;
|
||||
|
||||
@GetMapping("/plant")
|
||||
public List getPlants() {
|
||||
return plantRepository.findAll();
|
||||
}
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
If I navigate to /plant, the program returns a JSON-formatted string of the all of the plants stored in the database. I will use this as a foundation to later build into the project actual functionality.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Ficus Audrey"
|
||||
},
|
||||
{
|
||||
"name": "Lucky Bamboo"
|
||||
},
|
||||
{
|
||||
"name": "Chinese Money Plant"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
+++
|
||||
categories = ["software"]
|
||||
tags = ["java"]
|
||||
date = 2024-11-14T20:57:28Z
|
||||
description = ""
|
||||
draft = false
|
||||
slug = "spring-rest-api-pt-ii"
|
||||
title = "🍃 Developing Spring Rest API (pt. II)"
|
||||
author = "nicholas"
|
||||
+++
|
||||
|
||||
In my last log entry, I laid the groundwork for building a simple API that simulates plants. In this log entry I add the ability to create, delete, water, and fertilize plants. I am keeping the scope of this project very small and simple – my sustained attention is reserved for things I am truly passionate about, and this project is merely good fun. The gratification will come swift and total.
|
||||
|
||||
## Plant Simulation Operations
|
||||
Here I define the things that the plant simulator ought to be able to do. Before I begin writing much code, I want to understand the requirements and simulate how the simulation will work in my head (now in text). This will serve as a kind of proof of concept and clarify my thinking about the structure of the models and controllers.
|
||||
|
||||
* Plants will be managed with basic operations:
|
||||
* Create (plant seed)
|
||||
* Delete
|
||||
* Plants will be maintained with operations:
|
||||
* Water
|
||||
* Fertilize
|
||||
* Plants will progress through growth stages:
|
||||
* Seed
|
||||
* Germination
|
||||
* Seedling
|
||||
* Vegetating
|
||||
* Reproduction
|
||||
* Final
|
||||
* Plants will manage their own state `waterLevel`, `nutrientLevel`, and `growthStage` according to arbitrary values I set.
|
||||
* The waterLevel will be calculated according to the time of last watering.
|
||||
* Similarly, nutrientLevel will be calculated according to the time of last fertilizing.
|
||||
* The `growthStage` will be determined by the time that has passed since the plant was created.
|
||||
|
||||
|
||||
## Plant Class
|
||||
|
||||
### Constants:
|
||||
|
||||
* `HYDRATION_DECAY_RATE`
|
||||
* `NUTRIENT_DECAY_RATE`
|
||||
* These constants control how quickly hydration and nutrient levels decay over time.
|
||||
* `growthStageMap`: A `NavigableMap<Long, GrowthStage>` that maps time intervals (in hours since plant creation) to plant growth stages. It determines the plant's growth stage based on the time since creation.
|
||||
|
||||
### Fields:
|
||||
|
||||
* **id**
|
||||
* Unique identifier for the plant
|
||||
* auto-generated
|
||||
* **name**
|
||||
* The name of the plant.
|
||||
* **growthStage**
|
||||
* Enumerated strings representing the current growth stage of the plant
|
||||
* initialized to SEED
|
||||
* **created**
|
||||
* Timestamp indicating when the plant was created
|
||||
* default `CURRENT_TIMESTAMP`
|
||||
* **health**
|
||||
* Integer representing the plant's health
|
||||
* between 0 and 100
|
||||
* **hydration**
|
||||
* Integer representing the plant's hydration level
|
||||
* between 0 and 100
|
||||
* **nutrientLevel**
|
||||
* Integer representing the plant's nutrient level
|
||||
* between 0 and 100.
|
||||
* **lastWatered**
|
||||
* Timestamp of the last time the plant was watered.
|
||||
* **lastFertilized**
|
||||
* Timestamp of the last time the plant was fertilized.
|
||||
|
||||
|
||||
|
||||
### Methods - Getters and Setters:
|
||||
|
||||
* **getGrowthStage()**
|
||||
* Computes the plant’s growth stage based on the number of hours since creation by using `growthStageMap`. It returns the closest growth stage based on the elapsed time.
|
||||
* **getHealth()**
|
||||
* Calculates plant health as a weighted sum of hydration and nutrient level.
|
||||
* Hydration contributes 90% and nutrient level contributes 10%.
|
||||
* **getNutrientLevel()**
|
||||
* Computes the nutrient level based on the time elapsed since the last fertilization. Nutrients decay over time at a rate defined by `NUTRIENT_DECAY_RATE`.
|
||||
* **getHydration()**
|
||||
* Computes the hydration level based on the time elapsed since the last watering. Hydration decays over time at a rate defined by `HYDRATION_DECAY_RATE`.
|
||||
* **waterPlant()**
|
||||
* Increases the hydration level by 20, ensuring it doesn't exceed 100.
|
||||
* Updates the `lastWatered` timestamp.
|
||||
* **fertilizePlant()**
|
||||
* Increases the nutrient level by 20, ensuring it doesn't exceed 100.
|
||||
* Updates the `lastFertilized` timestamp.
|
||||
|
||||
### Validation:
|
||||
|
||||
* Uses `@Min` and `@Max` annotations to validate that `health`, `hydration`, and `nutrientLevel` are within the range [0, 100].
|
||||
|
||||
### JPA Annotations:
|
||||
|
||||
* **@Entity**
|
||||
* Marks the class as a JPA entity, making it a persistent object.
|
||||
* **@Table(name = "plant")**
|
||||
* Specifies the database table name as plant.
|
||||
* **@Id**
|
||||
* Marks `id` as the primary key.
|
||||
* **@GeneratedValue(strategy = GenerationType.IDENTITY)**
|
||||
* Automatically generates the `id` value.
|
||||
* **@Enumerated(EnumType.STRING)**
|
||||
* Maps the `growthStage` enum to a string in the database.
|
||||
* **@Column**
|
||||
* Specifies constraints for the columns, including nullable = false and custom column definitions (e.g., setting default values for created).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```java
|
||||
package plantsim.PlantSimulatorAPI;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
@Entity
|
||||
@Table(name = "plant")
|
||||
public class Plant {
|
||||
|
||||
private static final int HYDRATION_DECAY_RATE = 6;
|
||||
private static final int NUTRIENT_DECAY_RATE = 2;
|
||||
|
||||
private static final NavigableMap growthStageMap = new TreeMap<>();
|
||||
|
||||
static {
|
||||
growthStageMap.put(0L, GrowthStage.SEED);
|
||||
growthStageMap.put(1L, GrowthStage.GERMINATION);
|
||||
growthStageMap.put(2L, GrowthStage.SEEDLING);
|
||||
growthStageMap.put(5L, GrowthStage.VEGETATIVE);
|
||||
growthStageMap.put(24L, GrowthStage.FINAL);
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private GrowthStage growthStage = GrowthStage.SEED;
|
||||
|
||||
@Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
|
||||
private LocalDateTime created;
|
||||
|
||||
@Min(value = 0, message = "health must be at least 0")
|
||||
@Max(value = 100, message = "health must not exceed 100")
|
||||
@Column(nullable = false)
|
||||
private int health = 100;
|
||||
|
||||
@Min(value = 0, message = "hydration must be at least 0")
|
||||
@Max(value = 100, message = "hydration must not exceed 100")
|
||||
@Column(nullable = false)
|
||||
private int hydration = 100;
|
||||
|
||||
@Min(value = 0, message = "nutrient level must be at least 0")
|
||||
@Max(value = 100, message = "nutrient level must not exceed 100")
|
||||
@Column(nullable = false)
|
||||
private int nutrientLevel = 100;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime lastWatered = LocalDateTime.now();
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime lastFertilized = LocalDateTime.now();
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public GrowthStage getGrowthStage() {
|
||||
long hoursSinceCreated = Duration.between(created, LocalDateTime.now()).toHours();
|
||||
return growthStageMap.floorEntry(hoursSinceCreated).getValue();
|
||||
}
|
||||
|
||||
public int getHealth() {
|
||||
int hydrationWeight = 90;
|
||||
int fertilizationWeight = 10;
|
||||
|
||||
int weightedHydration = (getHydration() * hydrationWeight) / 100;
|
||||
int weightedFertilization = (getNutrientLevel() * fertilizationWeight) / 100;
|
||||
|
||||
return weightedHydration + weightedFertilization;
|
||||
}
|
||||
|
||||
public void setHealth(int health) {
|
||||
this.health = health;
|
||||
}
|
||||
|
||||
public int getNutrientLevel() {
|
||||
long minutesSinceLastFertilize = Duration.between(lastFertilized, LocalDateTime.now()).toMinutes();
|
||||
return Math.max(0, hydration - (int) (minutesSinceLastFertilize * NUTRIENT_DECAY_RATE));
|
||||
}
|
||||
|
||||
public void setNutrientLevel(int nutrientLevel) {
|
||||
this.nutrientLevel = nutrientLevel;
|
||||
}
|
||||
|
||||
public int getHydration() {
|
||||
long minutesSinceLastWater = Duration.between(lastWatered, LocalDateTime.now()).toMinutes();
|
||||
return Math.max(0, hydration - (int) (minutesSinceLastWater * HYDRATION_DECAY_RATE));
|
||||
}
|
||||
|
||||
public void setHydration(int hydration) {
|
||||
this.hydration = hydration;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastWatered() {
|
||||
return this.lastWatered;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastFertilized() {
|
||||
return this.lastFertilized;
|
||||
}
|
||||
|
||||
public Plant() {
|
||||
}
|
||||
|
||||
public void waterPlant() {
|
||||
this.hydration = Math.min(this.hydration + 20, 100);
|
||||
this.lastWatered = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void fertilizePlant() {
|
||||
this.nutrientLevel = Math.min(this.nutrientLevel + 20, 100);
|
||||
this.lastFertilized = LocalDateTime.now();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Defining the API Controller
|
||||
|
||||
```java
|
||||
package plantsim.PlantSimulatorAPI;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@RestController
|
||||
public class PlantController {
|
||||
|
||||
@Autowired
|
||||
private PlantRepository plantRepository;
|
||||
|
||||
@GetMapping("/plant")
|
||||
public List getPlants() {
|
||||
return plantRepository.findAll();
|
||||
}
|
||||
|
||||
@PostMapping("/plant")
|
||||
public ResponseEntity createPlant(@Valid @RequestBody Plant plant) {
|
||||
Plant newPlant = plantRepository.save(plant);
|
||||
return ResponseEntity.ok(newPlant);
|
||||
}
|
||||
|
||||
@DeleteMapping("/plants/{id}")
|
||||
public ResponseEntity deletePlant(@PathVariable Long id) {
|
||||
if (!plantRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
plantRepository.deleteById(id);
|
||||
return ResponseEntity.ok("Plant deleted");
|
||||
}
|
||||
|
||||
@PutMapping("/plant/{id}")
|
||||
public ResponseEntity updatePlant(@PathVariable Long id, @Valid @RequestBody Plant updatedPlant) {
|
||||
return plantRepository.findById(id).map(plant -> {
|
||||
plant.setName(updatedPlant.getName());
|
||||
Plant savedPlant = plantRepository.save(plant);
|
||||
return ResponseEntity.ok(savedPlant);
|
||||
}).orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PutMapping("/plant/{id}/water")
|
||||
public ResponseEntity waterPlant(@PathVariable Long id) {
|
||||
return plantRepository.findById(id).map(plant -> {
|
||||
plant.waterPlant();
|
||||
Plant savedPlant = plantRepository.save(plant);
|
||||
return ResponseEntity.ok(savedPlant);
|
||||
}).orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PutMapping("/plant/{id}/fertilize")
|
||||
public ResponseEntity fertilizePlant(@PathVariable Long id) {
|
||||
return plantRepository.findById(id).map(plant -> {
|
||||
plant.waterPlant();
|
||||
Plant savedPlant = plantRepository.save(plant);
|
||||
return ResponseEntity.ok(savedPlant);
|
||||
}).orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user