Files
web-log/container/site/content/posts/spring-rest-api-pt-ii/index.md
T
2025-07-05 15:36:34 +00:00

341 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
+++
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());
}
}
```