Merge remote-tracking branch 'origin/feature_json-gardenplan_M2' into feature_json-task-db_M2

# Conflicts:
#	build.gradle
This commit is contained in:
Gian-Andrea Hutter 2022-10-28 11:52:49 +02:00
commit bd2aa60128
10 changed files with 337 additions and 17 deletions

View File

@ -39,7 +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'
testImplementation 'org.mockito:mockito-core:4.3.+'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4'
}
test {

View File

@ -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<UserPlanting> 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<Crop> 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<Crop> 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;
}

View File

@ -0,0 +1,9 @@
package ch.zhaw.gartenverwaltung.io;
import java.io.IOException;
class InvalidJsonException extends IOException {
public InvalidJsonException(String reason) {
super(reason);
}
}

View File

@ -0,0 +1,140 @@
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;
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<Long, Crop> 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("yyyy-MM-dd");
LocalDateDeserializer dateDeserializer = new LocalDateDeserializer(dateFormat);
LocalDateSerializer dateSerializer = new LocalDateSerializer(dateFormat);
timeModule.addDeserializer(LocalDate.class, dateDeserializer);
timeModule.addSerializer(LocalDate.class, dateSerializer);
}
/**
* @see GardenPlan#getCrops()
*/
@Override
public List<Crop> getCrops() throws IOException {
if (idProvider == null) {
loadCropList();
}
return cropMap.values().stream().toList();
}
/**
* @see GardenPlan#getCropById(long)
*/
@Override
public Optional<Crop> 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<Long> 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<Crop> 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)
.registerModule(new Jdk8Module())
.writeValue(new File(dataSource.toURI()), cropMap.values());
} catch (URISyntaxException e) {
// TODO: Log
throw new IOException(INVALID_DATASOURCE_MSG, e);
}
}
}
}

View File

@ -0,0 +1,52 @@
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 double area = 1;
/**
* Default Constructor (needed for deserialization)
*/
public Crop() {
plantId = 0;
startDate = null;
}
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(double area) {
this.area = area;
return this;
}
// Getters
public Optional<Long> getCropId() {
return Optional.ofNullable(cropId);
}
public long getPlantId() { return plantId; }
public LocalDate getStartDate() { return startDate; }
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);
}
}

View File

@ -1,10 +0,0 @@
package ch.zhaw.gartenverwaltung.types;
import java.util.Date;
public record UserPlanting(
long plantId,
Date startDate,
int area
) {
}

View File

@ -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;

View File

@ -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": [
{

View File

@ -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
}
]

View File

@ -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<Plant> 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<String> names = testList.stream().map(Plant::name).collect(Collectors.toList());
List<String> expected = Arrays.asList("Potato","Early Carrot","Summertime Onion");
Assertions.assertEquals(expected,names);
}
@Test
@DisplayName("Check whether single access works.")
void getPlantById() {
Optional<Plant> 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<Plant> 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());
}
}