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