diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/GardenPlan.java b/src/main/java/ch/zhaw/gartenverwaltung/io/GardenPlan.java index 94e1346..0e04f9a 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/GardenPlan.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/GardenPlan.java @@ -1,12 +1,42 @@ package ch.zhaw.gartenverwaltung.io; -import ch.zhaw.gartenverwaltung.types.UserPlanting; +import ch.zhaw.gartenverwaltung.types.Crop; import java.io.IOException; import java.util.List; +import java.util.Optional; public interface GardenPlan { - List getPlantings(); - void savePlanting(UserPlanting planting) throws IOException; - void removePlanting(UserPlanting planting) throws IOException; + /** + * Yields a list of all {@link Crop}s in the database. + * + * @return A list of all {@link Crop}s in the database + * @throws IOException If there is a problem reading from or writing to the database + */ + List getCrops() throws IOException; + + /** + * Attempts to retrieve the {@link Crop} with the specified cropId. + * + * @param id The {@link Crop#cropId} to look for + * @return {@link Optional} of the found {@link Crop}, {@link Optional#empty()} if no entry matched the criteria + * @throws IOException If there is a problem reading from or writing to the database + */ + Optional getCropById(long id) throws IOException; + + /** + * Saves a Crop to the Database. + * + * @param crop The {@link Crop} to be saved + * @throws IOException If there is a problem reading from or writing to the database + */ + void saveCrop(Crop crop) throws IOException; + + /** + * Removes a Crop from the Database. + * + * @param crop The {@link Crop} to be removed + * @throws IOException If there is a problem reading from or writing to the database + */ + void removeCrop(Crop crop) throws IOException; } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/InvalidJsonException.java b/src/main/java/ch/zhaw/gartenverwaltung/io/InvalidJsonException.java new file mode 100644 index 0000000..9c3b8dc --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/InvalidJsonException.java @@ -0,0 +1,9 @@ +package ch.zhaw.gartenverwaltung.io; + +import java.io.IOException; + +class InvalidJsonException extends IOException { + public InvalidJsonException(String reason) { + super(reason); + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java new file mode 100644 index 0000000..c4e224c --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java @@ -0,0 +1,134 @@ +package ch.zhaw.gartenverwaltung.io; + +import ch.zhaw.gartenverwaltung.types.Crop; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class JsonGardenPlan implements GardenPlan { + private final URL dataSource = getClass().getResource("user-crops.json"); + + private IdProvider idProvider; + private Map cropMap = Collections.emptyMap(); + + /** + * Creating constant objects required to deserialize the {@link LocalDate} classes + */ + private final static JavaTimeModule timeModule = new JavaTimeModule(); + private final static String INVALID_DATASOURCE_MSG = "Invalid datasource specified!"; + static { + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yy-MM-dd"); + LocalDateDeserializer dateDeserializer = new LocalDateDeserializer(dateFormat); + timeModule.addDeserializer(LocalDate.class, dateDeserializer); + } + + /** + * @see GardenPlan#getCrops() + */ + @Override + public List getCrops() throws IOException { + if (idProvider == null) { + loadCropList(); + } + return cropMap.values().stream().toList(); + } + + /** + * @see GardenPlan#getCropById(long) + */ + @Override + public Optional getCropById(long id) throws IOException { + if (idProvider == null) { + loadCropList(); + } + return Optional.ofNullable(cropMap.get(id)); + } + + /** + * @see GardenPlan#saveCrop(Crop) + * + * Saves a crop to the database. + * If no {@link Crop#cropId} is set, one will be generated and + * the Crop is stored as a new entry. + */ + @Override + public void saveCrop(Crop crop) throws IOException { + if (idProvider == null) { + loadCropList(); + } + Long cropId = crop.getCropId().orElse(idProvider.incrementAndGet()); + cropMap.put(cropId, crop.withId(cropId)); + writeCropList(); + } + + /** + * @see GardenPlan#removeCrop(Crop) + */ + @Override + public void removeCrop(Crop crop) throws IOException { + if (idProvider == null) { + loadCropList(); + } + Optional cropId = crop.getCropId(); + if (cropId.isPresent()) { + cropMap.remove(cropId.get()); + writeCropList(); + } + } + + /** + * Loads the database from {@link #dataSource} and updates the cached data. + * + * @throws IOException If the database cannot be accessed or invalid data was read. + */ + private void loadCropList() throws IOException { + if (dataSource != null) { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(timeModule); + + List result; + result = mapper.readerForListOf(Crop.class).readValue(dataSource); + + // Turn list into a HashMap with structure cropId => Crop + cropMap = new HashMap<>(result.size()); + for (Crop crop : result) { + Long id = crop.getCropId() + .orElseThrow(() -> new InvalidJsonException(String.format("Invalid value for \"cropId\" in %s!", crop))); + cropMap.put(id, crop); + } + Long maxId = cropMap.isEmpty() ? 0L : Collections.max(cropMap.keySet()); + idProvider = new IdProvider(maxId); + } + } + + /** + * Writes the values from {@link #cropMap} to the {@link #dataSource} + * + * @throws IOException If the database cannot be accessed + */ + private void writeCropList() throws IOException { + if (dataSource != null) { + try { + new ObjectMapper() + .registerModule(timeModule) + .writeValue(new File(dataSource.toURI()), cropMap.values()); + } catch (URISyntaxException e) { + // TODO: Log + throw new IOException(INVALID_DATASOURCE_MSG, e); + } + } + } + +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java new file mode 100644 index 0000000..a55a010 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java @@ -0,0 +1,35 @@ +package ch.zhaw.gartenverwaltung.types; + +import java.time.LocalDate; +import java.util.Optional; + +public class Crop { + private Long cropId = null; + private final long plantId; + private final LocalDate startDate; + private int area = 1; + + public Crop(long plantId, LocalDate startDate) { + this.plantId = plantId; + this.startDate = startDate; + } + + // Builder-Style setter + public Crop withId(long cropId) { + this.cropId = cropId; + return this; + } + public Crop withArea(int area) { + this.area = area; + return this; + } + + // Getters + public Optional getCropId() { + return Optional.ofNullable(cropId); + } + + public long getPlantId() { return plantId; } + public LocalDate getStartDate() { return startDate; } + public int getArea() { return area; } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/UserPlanting.java b/src/main/java/ch/zhaw/gartenverwaltung/types/UserPlanting.java deleted file mode 100644 index 804d388..0000000 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/UserPlanting.java +++ /dev/null @@ -1,10 +0,0 @@ -package ch.zhaw.gartenverwaltung.types; - -import java.util.Date; - -public record UserPlanting( - long plantId, - Date startDate, - int area -) { -}