https://visualhunt.com/photo/167969/

XML w Javie (JAXB) cz. 1

Konkurs „Daj się poznać 2017” dla mnie się zakończył. Szczerze nie liczę na otrzymanie jakichkolwiek głosów od innych uczestników, bo za bardzo nie udzielałem się w społeczności. O moim udziale w „rywalizacji” przeczytasz w poprzednim wpisie. Tak jak już wspominałem, nie zamierzam porzucać projektu Sterownika Domowego, jednak nie będzie to jedyny temat, który w najbliższym czasie pojawi się na moim blogu. Czerpiąc dobre praktyki z założeń konkursu (mam na myśli regularne blogowanie), na najbliższe dwa miesiące za cel obrałem sobie 8 nowych postów, które mają pomóc mi usystematyzować wiedzę związaną z Javą. Oczywiście nie wykluczam również innych tematów, jednak czas pokaże jak będzie.

Na pierwszy wpis wybrałem temat związany z obsługą XML w Javie, bo to zagadnienie pojawiło się ostatnio na jednym z zadań rekrutacyjnych. Moje rozwiązanie nie opierało się ani o DOM, ani SAX, natomiast postanowiłem wykorzystać JAXB, którego podstawy dziś opiszę. Dlaczego akurat taki wybór? Jest to API wyższego poziomu, co przede wszystkim zwiększa szybkość implementacji docelowego rozwiązania i czystość kodu.

JAXB, czyli Java Architecture for XML Binding to API wchodzące w skład JRE (od wersji 1.6), które ma za zadanie ułatwiać pracę z XML, dostarczając rozwiązania do dwustronnej konwersji pomiędzy obiektami modelu i strukturą XML. Aktualnie dla Java SE 8, JAXB dostępny jest w wersji 2.2.8 (Wikipedia).

Wyróżnia się dwie podstawowe operacje w JAXB:
Marshaling – zamiana obiektów Java na strukturę XML
Un-marshaling – zmiana struktury XML na obiekty Java

Pokażę teraz jak to wygląda od strony praktycznej. Opiszę dwa różne sposoby dojścia do rozwiązania: wykorzystanie narzędzia xjc oraz ręczne utworzenie modelu. Wydaje mi się, że krótkie teksty na blogach czyta się przyjemniej, dlatego zdecydowałem się na podzielenie całego tematu na dwie części. W tym wpisie przybliżę pierwsze rozwiązanie, natomiast za parę dni pokażę jak ręcznie tworzy się model danych, wykorzystując adnotacje do opisu struktury.

Załóżmy, że w swojej aplikacji chciałbym przetwarzać tabelę kursów walut dostępną na stronach NBP. Jest to przykład o tyle ciekawy, że mogę przećwiczyć kila fajnych rzeczy za jednym razem. Po pierwsze zapoznam się z publicznym API NBP, na jego podstawie utworzę plik opisujący strukturę XML (*.xsd), po czym za pomocą narzędzia xjc wygeneruję model danych. W dalszej kolejności nauczę się pobierać dane w formacie XML wykorzystując HttpURLConnection. Gdzieś w międzyczasie będę chciał wykorzystać elementy języka, które pojawiły się Java 8.

API NBP udostępnia funkcje, które umożliwiają pobranie całych tabel kursów, pojedynczych walut oraz cen złota. W każdym przypadku istnieje możliwość parametryzacji zapytań, np. podając datę publikacji danych. Na potrzeby tego przykładu chcę pobierać całą, aktualnie obowiązującą tabelę kursów walut. Zapytanie, które zwróci mi takie dane wygląda jak poniżej.

http://api.nbp.pl/api/exchangerates/tables/A

Nie podałem formatu danych, w jakim chciałem je otrzymać, więc zgodnie z dokumentacją otrzymałem domyślny XML, na podstawie którego wygenerowana zostanie definicja XML Schema. Szukając najprostszego rozwiązania, można wykorzystać generatory dostępne w sieci, np. XSD/XML Schema Generator. Po tej operacji, dysponując plikiem *.xsd, można wygenerować model danych.

xjc nbp.xsd

Wynikiem działania powyższego polecenia są dwa pliki: ArrayOfExchangeRatesTable.java oraz ObjectFactory.java, przy czym pierwszy z nich zawiera model danych z którego dalej będę korzystał, natomiast w drugim znajdują się metody wytwórcze.

Kolejny krok to dołączenie pliku do projektu, zmiana domyślnego pakietu oraz nadpisanie metody toString() na przyjemniejszą dla oka. Od tego momentu można śmiało realizować właściwą logikę aplikacji, przetwarzającą kursy walut.

Punkt startowy aplikacji znajduje się w funkcji main() klasy CurrencyReader. Realizację zadania oddelegowałem do obiektu reader klasy CurrencyTableReader, którego metoda getRatesInTable() zwraca listę notowań walut (Rate) (poprawcie mnie proszę, jeśli to się nazywa inaczej), która jest wypisywana w konsoli.

public class CurrencyReader {
 
    public static void main(String args[]) {
        CurrencyTableReader reader = new CurrencyTableReader();
        List<Rate> rates = reader.getRatesInTable(TableTypeEnum.B);
        rates.forEach(System.out::println);
    }
}

Metoda getRatesInTable() jako argument przyjmuje zmienną typu wyliczeniowego, gdzie na sztywno określone są rodzaje dostępnych tabel. Analizując jej wnętrze można zauważyć obiekt table typu ArrayOfExchangeRatesTable, który to pochodzi z wygenerowanego wcześniej (za pomocą narzędzia xjc) modelu. Idąc dalej, implementacja funkcji readLastTable() dopuszcza sytuację, w której zwrócona wartość będzie równa null. Ma to miejsce w przypadku problemów z odczytem danych z NBP Web API. Aby uchronić się przed popularnym wyjątkiem NullPointerException zastosowałem klasę Optional parametryzowaną ArrayOfExchangeRatesTable. Wyrażenie lambda wewnątrz metody ifPresent() iteruje po pobranych danych, dodając notowania walut do zmiennej rates, będącej wynikiem funkcji.

public class CurrencyTableReader {
 
    public List<Rate> getRatesInTable(TableTypeEnum tableType) {
        List<Rate> rates = new ArrayList();
        ArrayOfExchangeRatesTable table = readLastTable(tableType);
 
        Optional<ArrayOfExchangeRatesTable> optionalTable = Optional.ofNullable(table);
        optionalTable.ifPresent((ArrayOfExchangeRatesTable t) -> {
            List<Rate> optionalRates = t.getExchangeRatesTable().getRates().getRate();
            optionalRates.forEach(rates::add);
        });
        return rates;
    }
    // (...)
}

Za pobieranie danych z NBP Web API odpowiada metoda readLastTable(), w której inicjowane jest połączenie wykorzystujące obiekt klasy HttpURLConnection oraz operacja unmarshallingu, czyli mapowania pobranej struktury XML na obiekt.

private ArrayOfExchangeRatesTable readLastTable(TableTypeEnum tableType) {
        String uri = "http://api.nbp.pl/api/exchangerates/tables/" + tableType;
        try {
            HttpURLConnection connection = prepareHttpConnection(new URL(uri));
            return unmarshallStreamContent(connection);
        } catch (IOException ex) {
            Logger.getLogger(CurrencyReader.class.getSimpleName()).log(Level.WARNING, "Problem z pobraniem danych");
        }
        return null;
    }

Metoda prepareHttpConnection() konfiguruje oraz otwiera połączenie. Znajduje się tu ustawienie typu żądania, które prawdę mówiąc jest nadmiarowe, bo domyślnie ustawiane jest GET oraz ustawienie akceptowalnej treści (application/xml).

private HttpURLConnection prepareHttpConnection(URL url) throws IOException, ProtocolException {
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("GET");
    connection.setRequestProperty("Accept", "application/xml");
    return connection;
}

Na sam koniec została metoda unmarshallStreamContent(), w której realizowane są operacje stanowiące główny temat tego wpisu. W tym miejscu wykonywane jest mapowanie struktury XML pobranej ze strumienia, na obiekt modelu. Do wykonania tej operacji potrzebny jest kontekst, inicjowany typem modelu danych, czyli klasą ArrayOfExchangeRatesTable. Na tym kontekście tworzony jest obiekt Unmarshaller, na którym wykonywana jest metoda unmarshal() pobierająca jako argument strumień ze strukturą XML. Po wykonaniu tej operacji zamykane jest połączenie. Konstrukcja try() robi za programistę świetną rzecz, bo nie musi się on przejmować zamykaniem strumienia, mniej jest niepotrzebnego kodu i staje się on przyjemniejszy dla oczu.

private ArrayOfExchangeRatesTable unmarshallStreamContent(HttpURLConnection connection) {
        try (InputStream xml = connection.getInputStream()) {
            JAXBContext jc = JAXBContext.newInstance(ArrayOfExchangeRatesTable.class);
            ArrayOfExchangeRatesTable table = (ArrayOfExchangeRatesTable) jc.createUnmarshaller().unmarshal(xml);
            System.out.println("Data notowania: " + table.getExchangeRatesTable().getEffectiveDate());
            connection.disconnect();
            return table;
        } catch (IOException ex) {
            Logger.getLogger(CurrencyReader.class.getSimpleName()).log(Level.WARNING, "Problem z odczytem ze strumienia");
        } catch (JAXBException ex) {
            Logger.getLogger(CurrencyReader.class.getSimpleName()).log(Level.WARNING, "Problem z konwersją na XML");
        }
        return null;
    }

Wynik działania programu pokazuje, że wszystko poszło zgodnie z planem.

GET Response Code :: 200
Data notowania: 2017-06-02
bat (Tajlandia) [THB]: 0.1092
dolar amerykański [USD]: 3.7323
euro [EUR]: 4.1882
...
rupia indonezyjska [IDR]: 2.8054E-4
rupia indyjska [INR]: 0.05788
won południowokoreański [KRW]: 0.003324
yuan renminbi (Chiny) [CNY]: 0.5475
SDR (MFW) [XDR]: 5.1696

Tak jak wspomniałem wcześniej, to jest część pierwsza wpisu. Serdecznie zapraszam do śledzenia mojego bloga, już niedługo pojawią się nowe treści związane z językiem JAVA.

Literatura obowiązkowa

JAXB hello world example
JAXB – Tutorial
JAXB Tutorial for Java XML Binding
Java HttpURLConnection Example – Java HTTP Request GET, POST

P.S. Zapraszam do drugiej części o JAXB: XML w Javie (JAXB) cz. 2

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *