From a29f8c7db790c3d82312f63b750b03bf7d1d5500 Mon Sep 17 00:00:00 2001 From: David Guler Date: Mon, 24 Oct 2022 13:42:31 +0200 Subject: [PATCH 1/4] Added IdProvider --- .../java/ch/zhaw/gartenverwaltung/io/IdProvider.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/ch/zhaw/gartenverwaltung/io/IdProvider.java diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/IdProvider.java b/src/main/java/ch/zhaw/gartenverwaltung/io/IdProvider.java new file mode 100644 index 0000000..5842f0d --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/IdProvider.java @@ -0,0 +1,11 @@ +package ch.zhaw.gartenverwaltung.io; + +public class IdProvider { + private long currentId; + public IdProvider(long initialValue) { + currentId = initialValue; + } + public long incrementAndGet() { + return ++currentId; + } +} From aceb6aa1e6c120f0ca0334857a57b3388a768e02 Mon Sep 17 00:00:00 2001 From: Elias Csomor Date: Mon, 24 Oct 2022 14:54:49 +0200 Subject: [PATCH 2/4] first tests and a typo corrected --- .../ch/zhaw/gartenverwaltung/io/plantdb.json | 2 +- .../io/JsonPlantDatabaseTest.java | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/test/java/ch/zhaw/gartenverwaltung/io/JsonPlantDatabaseTest.java diff --git a/src/main/resources/ch/zhaw/gartenverwaltung/io/plantdb.json b/src/main/resources/ch/zhaw/gartenverwaltung/io/plantdb.json index f513570..723b450 100644 --- a/src/main/resources/ch/zhaw/gartenverwaltung/io/plantdb.json +++ b/src/main/resources/ch/zhaw/gartenverwaltung/io/plantdb.json @@ -165,7 +165,7 @@ }, { "id": 2, - "name": "summertime onion", + "name": "Summertime Onion", "description": "Onion, (Allium cepa), herbaceous biennial plant in the amaryllis family (Amaryllidaceae) grown for its edible bulb. The onion is likely native to southwestern Asia but is now grown throughout the world, chiefly in the temperate zones. Onions are low in nutrients but are valued for their flavour and are used widely in cooking. They add flavour to such dishes as stews, roasts, soups, and salads and are also served as a cooked vegetable.", "lifecycle": [ { diff --git a/src/test/java/ch/zhaw/gartenverwaltung/io/JsonPlantDatabaseTest.java b/src/test/java/ch/zhaw/gartenverwaltung/io/JsonPlantDatabaseTest.java new file mode 100644 index 0000000..b554502 --- /dev/null +++ b/src/test/java/ch/zhaw/gartenverwaltung/io/JsonPlantDatabaseTest.java @@ -0,0 +1,79 @@ +package ch.zhaw.gartenverwaltung.io; + +import ch.zhaw.gartenverwaltung.types.HardinessZone; +import ch.zhaw.gartenverwaltung.types.Plant; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class JsonPlantDatabaseTest { + PlantDatabase testDatabase; + SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yyyy"); + + @BeforeEach + void connectToDb() { + testDatabase = new JsonPlantDatabase(); + } + + + @Test + @DisplayName("Check if results are retrieved completely") + void getPlantList() { + List testList; + try { + testList = testDatabase.getPlantList(HardinessZone.ZONE_8A); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (HardinessZoneNotSetException e) { + throw new RuntimeException(e); + } + Assertions.assertEquals(3, testList.size()); + + List names = testList.stream().map(Plant::name).collect(Collectors.toList()); + List expected = Arrays.asList("Potato","Early Carrot","Summertime Onion"); + Assertions.assertEquals(expected,names); + } + + @Test + @DisplayName("Check whether single access works.") + void getPlantById() { + Optional testPlant; + try { + testDatabase.getPlantList(HardinessZone.ZONE_8A); + testPlant = testDatabase.getPlantById(1); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (HardinessZoneNotSetException e) { + throw new RuntimeException(e); + } + Assertions.assertTrue(testPlant.isPresent()); + Assertions.assertEquals("Early Carrot", testPlant.get().name()); + } + + @Test + @DisplayName("Check for a nonexisting plant.") + void getPlantByIdMustFail() { + Optional testPlant; + try { + testDatabase.getPlantList(HardinessZone.ZONE_8A); + testPlant = testDatabase.getPlantById(99); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (HardinessZoneNotSetException e) { + throw new RuntimeException(e); + } + Assertions.assertFalse(testPlant.isPresent()); + + + } + +} + From 72b0d029d54c6648fcb53d17e4bccb0c07450f4f Mon Sep 17 00:00:00 2001 From: David Guler Date: Mon, 24 Oct 2022 19:19:45 +0200 Subject: [PATCH 3/4] #25 Initial implementation of the JsonGardenPlan Updated and renamed the Crop type to accommodate storage --- .../zhaw/gartenverwaltung/io/GardenPlan.java | 38 ++++- .../io/InvalidJsonException.java | 9 ++ .../gartenverwaltung/io/JsonGardenPlan.java | 134 ++++++++++++++++++ .../ch/zhaw/gartenverwaltung/types/Crop.java | 35 +++++ .../gartenverwaltung/types/UserPlanting.java | 10 -- 5 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 src/main/java/ch/zhaw/gartenverwaltung/io/InvalidJsonException.java create mode 100644 src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java create mode 100644 src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java delete mode 100644 src/main/java/ch/zhaw/gartenverwaltung/types/UserPlanting.java 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 -) { -} From 629e64143b2f68609696f94d4331b0416e930b18 Mon Sep 17 00:00:00 2001 From: David Guler Date: Wed, 26 Oct 2022 20:32:01 +0200 Subject: [PATCH 4/4] Fixed serialisation of cropId and startDate --- build.gradle | 1 + .../gartenverwaltung/io/JsonGardenPlan.java | 8 ++++++- .../ch/zhaw/gartenverwaltung/types/Crop.java | 23 ++++++++++++++++--- src/main/java/module-info.java | 2 +- .../zhaw/gartenverwaltung/io/user-crops.json | 20 ++++++++++++++++ 5 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 src/main/resources/ch/zhaw/gartenverwaltung/io/user-crops.json diff --git a/build.gradle b/build.gradle index 122bba0..6f8ef6d 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,7 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}") implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.4' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4' } test { diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java index c4e224c..5a25a0a 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java @@ -2,8 +2,10 @@ package ch.zhaw.gartenverwaltung.io; import ch.zhaw.gartenverwaltung.types.Crop; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import java.io.File; import java.io.IOException; @@ -29,9 +31,12 @@ public class JsonGardenPlan implements GardenPlan { 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"); + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd"); LocalDateDeserializer dateDeserializer = new LocalDateDeserializer(dateFormat); + LocalDateSerializer dateSerializer = new LocalDateSerializer(dateFormat); + timeModule.addDeserializer(LocalDate.class, dateDeserializer); + timeModule.addSerializer(LocalDate.class, dateSerializer); } /** @@ -123,6 +128,7 @@ public class JsonGardenPlan implements GardenPlan { try { new ObjectMapper() .registerModule(timeModule) + .registerModule(new Jdk8Module()) .writeValue(new File(dataSource.toURI()), cropMap.values()); } catch (URISyntaxException e) { // TODO: Log diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java index a55a010..fbd6286 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java @@ -7,7 +7,15 @@ public class Crop { private Long cropId = null; private final long plantId; private final LocalDate startDate; - private int area = 1; + private double area = 1; + + /** + * Default Constructor (needed for deserialization) + */ + public Crop() { + plantId = 0; + startDate = null; + } public Crop(long plantId, LocalDate startDate) { this.plantId = plantId; @@ -19,7 +27,7 @@ public class Crop { this.cropId = cropId; return this; } - public Crop withArea(int area) { + public Crop withArea(double area) { this.area = area; return this; } @@ -31,5 +39,14 @@ public class Crop { public long getPlantId() { return plantId; } public LocalDate getStartDate() { return startDate; } - public int getArea() { return area; } + public double getArea() { return area; } + + @Override + public String toString() { + return String.format("Crop [ cropId: %d, plantId: %d, startDate: %s, area: %f ]", + cropId, + plantId, + startDate, + area); + } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8e02fc7..00cf813 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -3,10 +3,10 @@ module ch.zhaw.gartenverwaltung { requires javafx.fxml; requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.datatype.jsr310; + requires com.fasterxml.jackson.datatype.jdk8; opens ch.zhaw.gartenverwaltung to javafx.fxml; opens ch.zhaw.gartenverwaltung.types to com.fasterxml.jackson.databind; -// opens ch.zhaw.gartenverwaltung.types to com.fasterxml.jackson.databind; exports ch.zhaw.gartenverwaltung; exports ch.zhaw.gartenverwaltung.types; exports ch.zhaw.gartenverwaltung.json; diff --git a/src/main/resources/ch/zhaw/gartenverwaltung/io/user-crops.json b/src/main/resources/ch/zhaw/gartenverwaltung/io/user-crops.json new file mode 100644 index 0000000..ebd1d2d --- /dev/null +++ b/src/main/resources/ch/zhaw/gartenverwaltung/io/user-crops.json @@ -0,0 +1,20 @@ +[ + { + "cropId": 0, + "plantId": 1, + "startDate": "2023-02-25", + "area": 0.5 + }, + { + "cropId": 1, + "plantId": 1, + "startDate": "2023-03-01", + "area": 0.5 + }, + { + "cropId": 2, + "plantId": 0, + "startDate": "2023-03-25", + "area": 1.5 + } +] \ No newline at end of file