Spis treści

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 Country i Case

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:

1
2
3
4
5
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.1</version>
</dependency>

i “wymagam” pakietu w module-info.java:

1
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:

1
2
3
4
5
6
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:

1
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:

1
2
requires com.fasterxml.jackson.databind;
exports com.kamilachyla.model to com.fasterxml.jackson.databind;

A po próbie uruchomienia i błędzie:

1
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.databind

dodaję kolejny:

1
2
3
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:

1
2
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:

1
2
mapper.configure(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Parsowanie LocalDate oraz int

Kolejny problem to parsowanie obiektu Case:

1
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.databind

Decyduję 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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:

1
2
3
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/

/covid-app-window-chart-filled.png

Repozytorium

Kod dostępny jest na GitHubie: covidstat.

Zmiany źródeł opisanie w tym wpisie zostały zawarte w komicie b87686d6f561.