Programowanie średnio-zaawansowane #26: Wczytywanie plików xml za pomocą JAXB.

JAXB (ang. Java Architecture for XML Binding) to wbudowana biblioteka Javy, która umożliwia łatwe przetwarzanie niewielkich plików xml. Dla osób, które nie miały dotąd do czynienia, z tymi typami plików, tłumaczę, że pliki xml oparte są o strukturę drzewiastą. Dane zawarte są w odpowiednich tagach (np. <root>rodzic</root>). Jeśli informacje przechowywane w nich zależne są od rodzica, to zagnieżdża się jeden tag w drugi. Brzmi to wszystko enigmatycznie, ale zaraz zademonstruję Ci, jak łatwo można zmapować proste klasy typu POJO do takich plików.

Załóżmy, że mamy dwie klasy zależne od siebie, które razem powinny odzwierciedlać pytania testowe na egzaminie. Klasa Question zawiera treść pytania, natomiast Answer posiada odpowiedzi do niego oraz flagę isCorrect, służącą do rozpoznania czy jest ono poprawne.

public class Question {
    private Integer id;
    private String description;
    private List<Answer> answerList;
        
    public Question() {}

    public Question(Integer id, String description) {
        this.id = id;
        this.description = description;
        this.answerList = new ArrayList<>();
    }
    
    public void populate(String description, boolean isCorrect, Integer id) {
        this.answerList.add(new Answer(id, description, isCorrect));
    }
    
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public List<Answer> getAnswerList() {
        return answerList;
    }
    
    public void setAnswerList(List<Answer> answerList) {
        this.answerList = answerList;
    }

    @Override
    public String toString() {
        return "Question{" + "id=" + id + ", description=" + description + ", answerList=" + answerList + '}';
    }
}
public class Answer {
    private Integer id;
    private String description;
    private boolean isCorrect;

    public Answer() {}
    
    public Answer(Integer id, String description, boolean isCorrect) {
        this.description = description;
        this.isCorrect= isCorrect;
        this.id = id;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
    
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public boolean isCorrect() {
        return isCorrect;
    }

    public void setCorrect(boolean isCorrect) {
        this.correct = isCorrect;
    }

    @Override
    public String toString() {
        return "Answer [description=" + description + ", isCorrect=" + correct + "]";
    }
}

Teraz powyższe obiekty zmapuję do pliku xml. Wpierw muszę stworzyć listę pytań do mojego quizu. W tym przypadku wymyśliłem dwa pytania geograficzne.

public static void main(String[] args) {
	Question questionOne = new Question(1, "Który z poniższych krajów należy do Unii Europejskiej?");
	questionOne.populate(1, "Polska", true);
	questionOne.populate(2, "Serbia", false);
	questionOne.populate(3, "Hiszpania", true);
	questionOne.populate(4, "Norwegia", false);

	Question questionTwo = new Question(2, "Stolica Niemiec jest?");
	questionTwo.populate(1, "Londyn", true);
	questionTwo.populate(2, "Paryż", false);
	questionTwo.populate(3, "Berlin", false);
	questionTwo.populate(4, "Frankfurt", false);
}

Skoro przykładowe dane zostały już zapisane, to należy teraz skorzystać ze specjalnych adnotacji, które pomogą frameworkowi JAXB zmapować zgodnie z moim oczekiwaniem dane z postaci obiektowej do formatu xml.

Wpierw nad nazwą klasy obowiązkowo musisz użyć adnotacji @XmlRootElement. Kolejna adnotacja @XmlAccessorType konfiguruje na jakiej podstawie odpowiednie tagi powinny być zmapowane (np. na podstawie pola). @XmlType w tym przypadku służy do ustalenia kolejności tagów, w jakiej powinny zostać zapisane do pliku. Ostatni @XmlAttribute konfiguruje atrybut, w taki sposób, aby był widoczny obok nazwy tagu (np. <answer id=”1″>). Jest to o tyle wygodne, że w ten sposób możesz umieszczać w tagu ważne dane, po których będziesz rozpoznawał każdy obiekt z osobna.

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder={"id", "description", "correct"})
public class Answer implements Serializable {
    private static final long serialVersionUID = 1L;
    @XmlAttribute
    private Integer id;

W klasie Question nic więcej się nie dzieje, poza tym że przezwałem korzeń z „Question” (nazwa klasy) na „QuestionPool”. Tak samo zrobiłem z listą pytań, która będzie w pliku rozpoznawana jako „answer”.

@XmlRootElement(name="QuestionPool")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder={"id", "description", "answerList"})
public class Question implements Serializable {
    private static final long serialVersionUID = 1L;
    @XmlAttribute    
    private Integer id;
    private String description;
    @XmlElement(name="answer")
    private List<Answer> answerList;

Skoro uzupełniłem obie klasy POJO, teraz należy zaimplementować metody konwertujące obiekt na plik i na odwrót. Pierwsza z nich convertObjectToXML stworzy plik na podstawie ścieżki oraz zawartości obiektu klasy Question, a następnie zapisze go za pomocą JAXB. Jak widzisz wpierw sprawdzam metodą checkFile, aby sprawdzić czy plik o podanej nazwie już istnieje. Następnie otwieram kontekst JAXB dla odpowiedniej klasy. W tym przykładzie jest to klasa Question. Nie będę ponawiał tej operacji dla Answer, ponieważ zawiera się on w każdym obiekcie typu Question. Teraz wystarczy stworzyć obiekt klasy Marshaller i dokonać konwersji za pomocą metody marshal. Ciekawostką jest ustawienie JAXB_FORMATTED_OUTPUT, które formatuje plik końcowy, tworząc strukturę drzewiastą.

public static StringWriter convertObjectToXML(Question entity, String filePath) {
        StringWriter stringWriter = new StringWriter();

        LOGGER.log(Level.INFO, "convertObjectToXML with path {0}", filePath);
        try {
            LOGGER.info("start writing..");
            try {
                checkFile(filePath);
            } catch (IOException e) {
            	LOGGER.log(Level.WARNING, e.getMessage(), e);
            }

            File file = new File(filePath);

            LOGGER.info("created file");
	    JAXBContext jaxbContext = JAXBContext.newInstance(Question.class);
            Marshaller jaxbMarshaller = jaxbContext.createMarshaller();

            jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            jaxbMarshaller.marshal(entity, file);
            jaxbMarshaller.marshal(entity, stringWriter);
        } catch (JAXBException e) {
        	LOGGER.log(Level.WARNING, e.getMessage(), e);
        }

        return stringWriter;
}

W następnej funkcji, dokonywana jest operacja dokładnie odwrotna. Teraz dane z wcześniej utworzonego pliku wczytuje do obiektów. Tym razem tworzony jest obiekt Unmarshaller, który za pomocą metody unmarchal konwertuje dane z postaci tekstowej do obiektowej. Jeśli chcesz dla sprawdzenia, wyświetlić na końcu zawartość obiektu w logach, to pamiętaj o metodzie toString oraz aby rezultat konwersji został poprawnie zrzutowany ((Question) jaxbUnmarshaller.unmarshal(file)).

public static Question convertXMLToObject(String path) {
        LOGGER.info("convertXMLToObject");
        Question entity = null;

        try {
            File file = new File(path);
            LOGGER.info("open file");
			JAXBContext jaxbContext = JAXBContext.newInstance(Question.class);

            Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
            entity = (Question) jaxbUnmarshaller.unmarshal(file);
            LOGGER.info("Result of mapping xml is " + entity.toString());

        } catch (JAXBException e) {
        	LOGGER.log(Level.WARNING, e.getMessage(), e);
        }

        return entity;
    }

Marshalling i unmarshalling to procesy przekształcania reprezentacji pamięci obiektu w format danych odpowiedni do ich przechowywania lub transmisji (w tym przypadku pliku xml). Istotne tutaj jest także pojęcie serializacji. Jeśli spojrzysz raz jeszcze na klasy POJO, to zauważysz zaimplementowany tam interfejs Serializable. Interfejs ten nie zawiera sygnatur żadnych metod, wymaga jedynie, abyś w swojej klasie stworzył pole serialVersionUID, do którego powinna zostać przypisana jakaś liczba całkowita. Tutaj w zależności od Twojego IDE, masz do wyboru, ręcznie przypisać wartość 1L (L ze względu, że to typ Long) lub wygenerowaną przez np. Eclipse. Wartość wygenerowana to specjalny hash, który powinien być unikatowy dla każdej klasy. Tworzony jest on na podstawie pól i ich wartości. Jeśli nie chcesz, aby jakieś pole przechodziło mechanizm marshallingu, to użyj na nim adnotacji @XmlTransient. Za pomocą serializacji wirtualna maszyna Javy rozpoznaje, czy wczytany do pamięci obiekt jest poprawny. Przypomina to trochę aplikację do ściągania plików z Internetu, która po zaciągnięciu całego pliku sprawdza, czy wartość checksum jest prawdziwa. W ten sposób wiadomo, czy plik ściągnął się poprawnie czy nie. Tutaj mechanizm jest podobny, tylko dotyczy Javy. Wartość 1L, użyta przeze mnie, oznacza, że serializacja zawsze się powiedzie, co jest trochę oszustwem. W przypadku kodu komercyjnego, zawsze używaj więc, wartości wygenerowanej.

Przykładowa implementacja metody pomocniczej checkFile, służącej do sprawdzenia czy ścieżka istnieje. Podobnego typu kod analizowałem w lekcjach odnośnie pakietów IO i NIO, także nic co tu znajdziesz nie powinno być dla Ciebie zaskoczeniem.

private static void checkFile(String filePath) throws IOException {
        Path path = Paths.get(filePath);
        if (Files.exists(path)) {
            LOGGER.info("File exist..");
            if (Files.isWritable(path)) {
                Files.deleteIfExists(path);
                LOGGER.info("File deleted..");              
            } else {
                LOGGER.info("File is not writable..");
            }
        } else {
            LOGGER.info("File is not exist..");            
        }
    }

Na koniec standardowa metoda main:

public static void main(String[] args) {
	String directory = "c:\\data\\";
        // Inicjalizacja obiektow questionOne i questionTwo 
	convertObjectToXML(questionOne, directory + "question1.xml");
	convertObjectToXML(questionTwo, directory + "question2.xml");
	convertXMLToObject(directory + "question1.xml");
	convertXMLToObject(directory + "question2.xml");
}

Wynik na konsoli:

cze 10, 2020 6:31:34 PM advanced.xml.JaxbUsage convertObjectToXML
INFO: convertObjectToXML with path c:\data\question1.xml
cze 10, 2020 6:31:34 PM advanced.xml.JaxbUsage convertObjectToXML
INFO: start writing..
cze 10, 2020 6:31:34 PM advanced.xml.JaxbUsage checkFile
INFO: File exist..
cze 10, 2020 6:31:34 PM advanced.xml.JaxbUsage checkFile
INFO: File deleted..
cze 10, 2020 6:31:34 PM advanced.xml.JaxbUsage convertObjectToXML
INFO: created file
cze 10, 2020 6:31:35 PM advanced.xml.JaxbUsage convertObjectToXML
INFO: convertObjectToXML with path c:\data\question2.xml
cze 10, 2020 6:31:35 PM advanced.xml.JaxbUsage convertObjectToXML
INFO: start writing..
cze 10, 2020 6:31:35 PM advanced.xml.JaxbUsage checkFile
INFO: File exist..
cze 10, 2020 6:31:35 PM advanced.xml.JaxbUsage checkFile
INFO: File deleted..
cze 10, 2020 6:31:35 PM advanced.xml.JaxbUsage convertObjectToXML
INFO: created file
cze 10, 2020 6:31:35 PM advanced.xml.JaxbUsage convertXMLToObject
INFO: convertXMLToObject
cze 10, 2020 6:31:35 PM advanced.xml.JaxbUsage convertXMLToObject
INFO: open file
cze 10, 2020 6:31:36 PM advanced.xml.JaxbUsage convertXMLToObject
INFO: Result of mapping xml is Question{id=1, description=Który z poniższych krajów należy do Unii Europejskiej?, answerList=[Answer [description=Polska, isCorrect=true], Answer [description=Serbia, isCorrect=false], Answer [description=Hiszpania, isCorrect=true], Answer [description=Norwegia, isCorrect=false]]}
cze 10, 2020 6:31:36 PM advanced.xml.JaxbUsage convertXMLToObject
INFO: convertXMLToObject
cze 10, 2020 6:31:36 PM advanced.xml.JaxbUsage convertXMLToObject
INFO: open file
cze 10, 2020 6:31:36 PM advanced.xml.JaxbUsage convertXMLToObject
INFO: Result of mapping xml is Question{id=2, description=Stolica Niemiec jest?, answerList=[Answer [description=Londyn, isCorrect=true], Answer [description=Paryż, isCorrect=false], Answer [description=Berlin, isCorrect=false], Answer [description=Frankfurt, isCorrect=false]]}

Przykładowa treść wygenerowanego pliku xml:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<QuestionPool id="1">
    <description>Który z poniższych krajów należy do Unii Europejskiej?</description>
    <answer id="1">
        <description>Polska</description>
        <correct>true</correct>
    </answer>
    <answer id="2">
        <description>Serbia</description>
        <correct>false</correct>
    </answer>
    <answer id="3">
        <description>Hiszpania</description>
        <correct>true</correct>
    </answer>
    <answer id="4">
        <description>Norwegia</description>
        <correct>false</correct>
    </answer>
</QuestionPool>

Nie przejmuj się pierwszą linijką, jest to standardowy nagłówek dodawany do wszystkich plików xmlowych.

*Więcej informacji o plikach xml: https://www.w3schools.com/xml/xml_whatis.asp

Dodaj komentarz