341 lines
11 KiB
Markdown
341 lines
11 KiB
Markdown
+++
|
||
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());
|
||
}
|
||
}
|
||
```
|
||
|
||
|
||
|