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

11 KiB
Raw Blame History

+++ 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).
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

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());
	}
}