delete Java posts (refs #8)
Build & Push Hugo Site Image / Build & Push Image (push) Successful in 14s

This commit is contained in:
2025-07-26 09:19:11 -05:00
parent c5c0e27a65
commit 98526ebacd
2 changed files with 0 additions and 519 deletions
@@ -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 plants 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());
}
}
```