Prosta aplikacja w javafx (2) - czytamy dane z pliku
Prototyp aplikacji działający na statycznych, “zhardkodowanych” danych (nasz HamdmadeService oferuje gotowe Javowe obiekty), wydaje się działać. Jednak prawdziwe dane istnieją formacie JSON. Spróbujmy więc wczytać (i wyświetlić) dane w tym formacie.
Czytanie danych z pliku
Na razie jeszcze nie będziemy się jeszcze łączyć z internetem. Będziemy czytać dane z plików:
- lista krajów będzie odczytywana z pliku covidstat/src/main/resources/countries.json
- dane dla kraju będą odczytywane z pliku
<slug>.json, gdzie<slug>to pole “slug” z obiektu opisującego kraj w pliku countries.json, na przykład: - dla Polski plik będzie w covidstat/src/main/resources/poland.json
- dla Niemiec plik będzie w covidstat/src/main/resources/germany.json
Dla uproszczenia, na liście krajów umieściłam tylko Polskę i Niemcy, i ściągnęłam dane szczegółowe również tylko dla tych krajów.
Stworzę nową implementację interfejsu CovidService o oryginalnej nazwie FileService, która:
- odczyta odpowiednie pliki w katalogu resources
- zdeserializuje je do rekordów
CountryiCase
Parsowanie JSON-a
Wybór biblioteki
Użyję biblioteki, która jest de-facto standardem w zakresie (de)serializacji jsona, czyli fasterxml i założę, że wszystko pójdzie jak z płatka.
Dodaję do pom.xml najświeższy jackson-databind:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.1</version>
</dependency>i “wymagam” pakietu w module-info.java:
requires com.fasterxml.jackson.databind;Ponieważ nie do końca jestem pewna, jak jackson poradzi sobie z deserializacją do rekordów, wygooglowuję informację o wspieraniu rekordów i na wszelki wypadek wskazuję pola rekordu jako @JsonProperty:
public record Country(
@JsonProperty("Country") String name,
@JsonProperty("Slug") String slug,
@JsonProperty("ISO2") String iso2) {
public static final Country UNKNOWN = new Country("UNKNOWN", "UNKNOWN", "UNKNOWN");
}Czy to wystarczy?
Eksport modułu z rekordami do Jacksona
Próba uruchomienia kończy się wyjątkiem:
java.lang.reflect.InaccessibleObjectException: Unable to make public com.kamilachyla.model.Country(java.lang.String,java.lang.String,java.lang.String) accessible: module covidstat does not "exports com.kamilachyla.model" to module com.fasterxml.jackson.databindŚwietny opis błędu - wiem, co robić; dodaję jeszcze jeden wiersz do module-info.java:
requires com.fasterxml.jackson.databind;
exports com.kamilachyla.model to com.fasterxml.jackson.databind;A po próbie uruchomienia i błędzie:
java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.lang.String com.kamilachyla.model.Country.name accessible: module covidstat does not "opens com.kamilachyla.model" to module com.fasterxml.jackson.databinddodaję kolejny:
requires com.fasterxml.jackson.databind;
exports com.kamilachyla.model to com.fasterxml.jackson.databind;
opens com.kamilachyla.model to com.fasterxml.jackson.databind;Lista krajów deserializuje się poprawnie.
Parsowanie obiektów Case
Nieznane właściwości
Problem z “nieznanymi” właściwościami:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "Country" (class com.kamilachyla.model.Country), not marked as ignorable (3 known properties: "name", "iso2", "slug"])
at [Source: (BufferedInputStream); line: 6, column: 6] (through reference chain: java.util.ArrayList[0]->com.kamilachyla.model.Country["Country"])rozwiązuję wygooglowując tutorial na Jencov.com i dodając do kodu:
mapper.configure(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);Parsowanie LocalDate oraz int
Kolejny problem to parsowanie obiektu Case:
java.lang.reflect.InaccessibleObjectException: Unable to make field private final int java.time.LocalDate.year accessible: module java.base does not "opens java.time" to module com.fasterxml.jackson.databindDecyduję się napisać własny deserializator do pól w klasie Case. Tym bardziej, że pola w klasie Country to LocalDate oraz wartości int.
Wystarczy rzut oka na tutorial Baeldung i jedziemy!
Deserializator dla rekordu Case
Implementuję deserializator CaseDeserializer:
public class CaseDeserializer extends StdDeserializer<Case> {
public CaseDeserializer() {
this(null);
}
public CaseDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Case deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
int confirmed = node.get("Confirmed").intValue();
int deaths = node.get("Deaths").intValue();
int recovered = node.get("Recovered").intValue();
int active = node.get("Active").intValue();
String dateStr = node.get("Date").asText();
LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_DATE_TIME);
return new Case(date, confirmed, deaths, recovered, active);
}
}Rejestracja deserializatora w mapperze
Teraz już tylko rejestruję CaseDeserializer w moim mapperze w klasie FileService:
SimpleModule module = new SimpleModule();
module.addDeserializer(Case.class, new CaseDeserializer());
mapper.registerModule(module);Screenshot - wykres pełnego zakresu danych
Oto wykres wszystkich serii danych dla rzeczywistych danych ściągniętych ze źródła https://api.covid19api.com/

Repozytorium
Kod dostępny jest na GitHubie: covidstat.
Zmiany źródeł opisanie w tym wpisie zostały zawarte w komicie b87686d6f561.
This text is part of the series javafx-covid...