Читаем на Java XML-файл с помощью SAX-парсера

Разработка на Java Просмотров: 1829

User Rating: 0 / 5

В этой статье мы посмотрим, как прочитать XML-файл при помощи SAX-парсера на Java. Вкратце объясню, что означает аббревиатура SAX - она взята по первым буквам от Simple API for XML, т.е. фактически это можно перевести как Простой программный интерфейс для работы с XML. Этот программный интерфейс, или по-другому API, и используется для того, чтобы читать (парсить) XML-документы.

Стоит иметь в виду, что помимо SAX существует также и другой термин DOM, который расшифровывается как Document Object Model, или Объектная модель документа. Для DOM также существует парсер, но алгоритм чтения данных из XML-файла существенно отличается между SAX и DOM. В первую очередь эти отличия касаются загрузки XML-документа в память программы - в то время как SAX парсер построен на основе событий, которые вызываются в процессе чтения документа, DOM загружает весь XML-документ в память программы, чтобы его распарсить (т.е. прочитать). Поэтому если перед вами стоит задача прочитать огромный по объему XML-файл, то учтите, что DOM может привести к тому, что память может переполниться, а SAX будет последовательно читать документ, несмотря на его объем.

Итак, перейдем к примеру реализации на Java. Пусть у нас есть задача считывания из XML-файла информации о книгах и авторах книг, которые хранятся в некоторой библиотеке. В процессе чтения из XML-файла мы бы хотели загрузить информацию обо всех книгах и авторах из соответствующих элементов XML-документа и инициализировать соответствующие этим сущностям Java-классы для более удобного доступа в нашей программе и управления этими данными.

Давайте возьмём следующий тестовый XML-файл, который мы будем впоследствии читать с помощью SAX-парсера на Java:

<book_library>
    <books>
        <book>
            <author_name>Лев Николаевич Толстой</author_name>
            <title>Война и мир</title>
            <date_published>1869</date_published>
        </book>
        <book>
            <author_name>Фёдор Михайлович Достоевский</author_name>
            <title>Преступление и наказание</title>
            <date_published>1866</date_published>
        </book>
        <book>
            <author_name>Александр Сергеевич Пушкин</author_name>
            <title>Евгений Онегин</title>
            <date_published>1833</date_published>
        </book>
    </books>
    <authors>
        <author>
            <name>Лев Николаевич Толстой</name>
            <date_born>1828</date_born>
        </author>
        <author>
            <name>Александр Сергеевич Пушкин</name>
            <date_born>1799</date_born>
        </author>
        <author>
            <name>Фёдор Михайлович Достоевский</name>
            <date_born>1821</date_born>
        </author>
    </authors>
</book_library>

Как видите, в файле есть два основных блока элементов - books и authors с данными о книгах и авторах.

Давайте теперь приготовим простые Java-классы, которые соответствуют структуре этого XML-файла: 

Ниже представлен код этих классов:

Класс Book:

package ru.allineed.samples.xml;

public class Book {
    private String title;
    private String authorName;
    private String datePublished;

    public void setTitle(String title) {
        this.title = title;
    }

    public void setAuthorName(String authorName) {
        this.authorName = authorName;
    }

    public void setDatePublished(String datePublished) {
        this.datePublished = datePublished;
    }

    @Override
    public String toString() {
        return "Book{" +
                "title='" + title + '\'' +
                ", authorName='" + authorName + '\'' +
                ", datePublished='" + datePublished + '\'' +
                '}';
    }
}

Класс Author:

package ru.allineed.samples.xml;

public class Author {
    private String name;
    private String dateBorn;

    public void setName(String name) {
        this.name = name;
    }

    public void setDateBorn(String dateBorn) {
        this.dateBorn = dateBorn;
    }

    @Override
    public String toString() {
        return "Author{" +
                "name='" + name + '\'' +
                ", dateBorn='" + dateBorn + '\'' +
                '}';
    }
}

Отлично, теперь у нас готовы простые классы (или POJO-классы от Plain Old Java Object - "старый добрый Java-объект") для хранения информации о книге и авторе. Но сами по себе классы не столь интересны, т.к. дают лишь способ получить контейнер для наших данных, которые будем считывать из XML.

Давайте ещё создадим класс BookLibrary ("книжная библиотека"), который будет управлять всеми данными о книгах и авторах, храня их в двух отдельных списках. Код этого класса представлен ниже:

package ru.allineed.samples.xml;

import java.util.ArrayList;
import java.util.List;

public class BookLibrary {
    private List<Author> authors;
    private List<Book> books;

    public void createAuthorsList() {
        authors = new ArrayList<>();
    }

    public void createAuthor() {
        authors.add(new Author());
    }

    public void createBooksList() {
        books = new ArrayList<>();
    }

    public void createBook() {
        books.add(new Book());
    }

    private void validateBooks() {
        if (books == null || books.isEmpty()) {
            throw new IllegalStateException("We don't have any books in the library!");
        }
    }
    private Book getCurrentBook() {
        validateBooks();
        return books.get(books.size() - 1);
    }

    private void validateAuthors() {
        if (authors == null || authors.isEmpty()) {
            throw new IllegalStateException("We don't have any authors in the library!");
        }
    }

    private Author getCurrentAuthor() {
        validateAuthors();
        return authors.get(authors.size() - 1);
    }

    public void setCurrentBookTitle(String title) {
        getCurrentBook().setTitle(title);
    }
    public void setCurrentBookAuthorName(String authorName) {
        getCurrentBook().setAuthorName(authorName);
    }

    public void setCurrentBookDatePublished(String datePublished) {
        getCurrentBook().setDatePublished(datePublished);
    }

    public void setCurrentAuthorName(String name) {
        getCurrentAuthor().setName(name);
    }

    public void setCurrentAuthorDateBorn(String dateBorn) {
        getCurrentAuthor().setDateBorn(dateBorn);
    }

    public void printAllBooks(String title) {
        validateBooks();
        System.out.println(title);
        books.forEach(System.out::println);
    }

    public void printAllAuthors(String title) {
        validateAuthors();
        System.out.println(title);
        authors.forEach(System.out::println);
    }
}

Разберём этот класс и его особенности (если по коду всё понятно - можете пропустить эту часть со списком):

Теперь, когда у нас готовы все необходимые классы для хранения данных из XML и управления ими, нам нужно сделать главное - написать реализацию SAX-парсера.

Для этого нужно создать класс обработчика событий при чтении парсером XML-файла, унаследовать его от стандартного Java-класса org.xml.sax.helpers.DefaultHandler и переопределить следующие  его методы:

Давайте назовём наш новый класс обработчика XmlBookLibraryHandler. Ниже код класса:

package ru.allineed.samples.xml;

import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

public class XmlBookLibraryHandler extends DefaultHandler {
    public static final String BOOKS_NODE = "books";
    public static final String BOOK_NODE = "book";
    public static final String AUTHOR_NAME_NODE = "author_name";
    public static final String TITLE_NODE = "title";
    public static final String DATE_PUBLISHED_NODE = "date_published";
    public static final String AUTHORS_NODE = "authors";
    public static final String AUTHOR_NODE = "author";
    public static final String NAME_NODE = "name";
    public static final String DATE_BORN_NODE = "date_born";
    private StringBuilder currentElementText;
    private BookLibrary library;
    public BookLibrary getLibrary() {
        return library;
    }

    @Override
    public void startDocument() throws SAXException {
        library = new BookLibrary();
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (currentElementText == null) {
            return;
        }
        currentElementText.append(ch, start, length);
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        switch (qName) {
            case BOOKS_NODE:
                library.createBooksList();
                break;
            case BOOK_NODE:
                library.createBook();
                break;
            case AUTHORS_NODE:
                library.createAuthorsList();
                break;
            case AUTHOR_NODE:
                library.createAuthor();
                break;
            case AUTHOR_NAME_NODE:
            case TITLE_NODE:
            case DATE_PUBLISHED_NODE:
            case NAME_NODE:
            case DATE_BORN_NODE:
                currentElementText = new StringBuilder();
                break;
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        switch (qName) {
            case AUTHOR_NAME_NODE:
                library.setCurrentBookAuthorName(currentElementText.toString());
                break;
            case TITLE_NODE:
                library.setCurrentBookTitle(currentElementText.toString());
                break;
            case DATE_PUBLISHED_NODE:
                library.setCurrentBookDatePublished(currentElementText.toString());
                break;
            case NAME_NODE:
                library.setCurrentAuthorName(currentElementText.toString());
                break;
            case DATE_BORN_NODE:
                library.setCurrentAuthorDateBorn(currentElementText.toString());
                break;
        }
    }
}

Далее будет разбор кода, который мы написали внутри этого обработчика (если вам и так всё понятно, можете пропустить эту часть):

Почти всё готово! Мы добрались до финальной части, теперь мы должны лишь создать какую-то тестовую программу для проверки нашего парсера, где мы создадим экземпляр парсера и передадим ему наш обработчик. Я назову новый класс  XmlParserExample, а ниже его код:

package ru.allineed.samples.xml;

import org.xml.sax.SAXException;
import ru.allineed.samples.common.OutputUtils;
import ru.allineed.samples.config.Localization;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.io.InputStream;

public class XmlParserExample {
    public static void main(String[] args) {
        OutputUtils.printSampleTitle(
                "Читаем на Java файл XML с помощью SAX-парсера",
                "Reading an XML file with Java using the SAX parser",
                "https://allineed.ru/development/java-development/66-java-reading-xml-via-sax-parser");

        SAXParserFactory parserFactory = SAXParserFactory.newInstance();
        try {
            SAXParser parser = parserFactory.newSAXParser();
            XmlBookLibraryHandler xmlBookLibraryHandler = new XmlBookLibraryHandler();

            try (InputStream ins = XmlParserExample.class.getClassLoader().getResourceAsStream("xml/xml_parser_example.xml")) {
                parser.parse(ins, xmlBookLibraryHandler);
                BookLibrary library = xmlBookLibraryHandler.getLibrary();

                String authorsTitle = Localization.getLocalized(
                        "Зарегистрированные в библиотеке авторы:",
                        "Authors registered in the Library:");

                library.printAllAuthors(authorsTitle);
                System.out.println();

                String booksTitle = Localization.getLocalized(
                        "Зарегистрированные в библиотеке книги:",
                        "Books registered in the Library:");
                library.printAllBooks(booksTitle);

            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        } catch (ParserConfigurationException | SAXException e) {
            e.printStackTrace();
        }
    }
}

Разберём ключевую логику этого класса.

В следующей строке мы создаём так называемую фабрику по производству SAX-парсеров.

SAXParserFactory parserFactory = SAXParserFactory.newInstance();

Далее, в блоке try (поскольку может произойти какое-то исключение связанное с конфигурацией парсера или с самим парсингом XML-файла), мы создаём сам SAX-парсер, а также экземпляр нашего класса-обработчика для парсера:

SAXParser parser = parserFactory.newSAXParser();
XmlBookLibraryHandler xmlBookLibraryHandler = new XmlBookLibraryHandler();

В этой строке с try мы обращаемся к ресурсу (это наш тестовый XML-файл), который должен располагаться в директории /src/main/resources/xml/, через загрузчик классов для текущего класса:

try (InputStream ins = XmlParserExample.class.getClassLoader().getResourceAsStream("xml/xml_parser_example.xml")) {
 // ...
}

Когда получен входной поток (InputStream), можно начинать парсинг XML-файла, вызывая метод parse у парсера и передавая ему входной поток и экземпляр xmlBookLibraryHandler нашего класса-обработчика:

parser.parse(ins, xmlBookLibraryHandler);

После того, как парсинг завершится, мы можем запросить экземпляр нашей библиотеки у обработчика, а также вызвать у него методы, которые выведут на экран всех авторов и все книги, которые мы считали из файла:

BookLibrary library = xmlBookLibraryHandler.getLibrary();

String authorsTitle = Localization.getLocalized(
		"Зарегистрированные в библиотеке авторы:",
		"Authors registered in the Library:");

library.printAllAuthors(authorsTitle);
System.out.println();

String booksTitle = Localization.getLocalized(
		"Зарегистрированные в библиотеке книги:",
		"Books registered in the Library:");
library.printAllBooks(booksTitle);

Если запустить пример, то вы увидите следующий вывод в консоли:

========================================================================================================================
>> Запуск примера для статьи "Читаем на Java файл XML с помощью SAX-парсера"
>> Ссылка на статью: https://allineed.ru/development/java-development/66-java-reading-xml-via-sax-parser
========================================================================================================================
Зарегистрированные в библиотеке авторы:
Author{name='Лев Николаевич Толстой', dateBorn='1828'}
Author{name='Александр Сергеевич Пушкин', dateBorn='1799'}
Author{name='Фёдор Михайлович Достоевский', dateBorn='1821'}

Зарегистрированные в библиотеке книги:
Book{title='Война и мир', authorName='Лев Николаевич Толстой', datePublished='1869'}
Book{title='Преступление и наказание', authorName='Фёдор Михайлович Достоевский', datePublished='1866'}
Book{title='Евгений Онегин', authorName='Александр Сергеевич Пушкин', datePublished='1833'}

Process finished with exit code 0

Как видим, все наши авторы и книги были считаны корректно из файла и выведены на консоль.

Внизу вы найдете ссылку на пример со всеми классами, который есть в нашем Git-репозитории, а также инструкцию по запуску всех примеров из статей сайта.

На этом всё, спасибо за внимание и удачи. Напишите в комментариях впечатления о статье, о своём опыте работы с SAX-парсерами. Насколько часто используете их в своих программах? Какому виду парсеров больше отдаёте предпочтение - SAX или DOM?