JavaRush /Java блог /Random UA /Основи XML для програмістів Java. Частина 3.2 із 3 - DOM
Ярослав
40 рівень
Днепр

Основи XML для програмістів Java. Частина 3.2 із 3 - DOM

Стаття з групи Random UA
<h2>Вступ</h2>Привіт всім читачам статті, ця частина присвячена DOM. Наступна буде присвячена JAXB і на цьому цикл основ XML буде завершений. Спочатку буде трохи теорії, а далі лише практика. Давайте приступати. <h2>DOM (Document Object Model) - ТЕОРІЯ</h2>DOM-обробник влаштований так, що він зчитує відразу весь XML і зберігає його, створюючи ієрархію у вигляді дерева, за якою ми можемо спокійно рухатися і отримувати доступ до потрібних нам елементів . Таким чином, ми можемо, маючи посилання на верхній елемент, отримати всі посилання на його внутрішні елементи. При чому елементи, які всередині елемента – діти цього елемента, а він – їхній батько. Одного разу вважаючи весь XML на згадку, ми просто подорожуватимемо його структурою і виконувати потрібні нам дії. Трохи вже про програмну частину DOM в Java: DOM має безліч інтерфейсів, які створені, щоб описувати різні дані. Всі ці інтерфейси успадковують один спільний інтерфейс - Node (вузол). Тому, насправді, найчастіший тип даних у DOM – це Node (вузол), який може бути всім. Кожен Node має такі корисні методи для отримання інформації:
  1. getNodeName- Отримати ім'я вузла.
  2. getNodeValue- Отримати значення вузла.
  3. getNodeType- Отримати тип вузла.
  4. getParentNode- Отримати вузол, всередині якого знаходиться даний вузол.
  5. getChildNodes– отримати всі похідні вузли (вузли, які всередині цього вузла).
  6. getAttributes- Отримати всі атрибути вузла.
  7. getOwnerDocument- Отримати документ цього вузла.
  8. getFirstChild/getLastChild– отримати перший/останній похідний вузол.
  9. getLocalName– корисно при обробці просторів імен, щоб отримати ім'я без префіксу.
  10. getTextContent– повертає весь текст усередині елемента та всіх елементів усередині цього елемента, включаючи переноси рядків та пробіли.
Примітка по 9 методу: він завжди повертатиме null, якщо в DocumentFactory ви не скористалися методом setNamespaceAware(true), щоб запустити обробку просторів імен. Тепер важлива деталь: методи спільні для всіх Node, але в Node у нас може бути як елемент, так і атрибут. І тут питання: яке значення може мати елемент? Які похідні вузли можуть мати атрибут? І інші не стикування. А все досить просто: кожен метод працюватиме в залежності від типу Node . Достатньо використовувати логіку, звичайно, щоб не заплутатися. Наприклад: які атрибути можуть бути в атрибутів? Яке ще значення елемента? Однак, щоб самому не пробувати все, в офіційних доках є дуже корисна табличка для кожного методу в залежності від типу Node: Якість погана вийшла, тому посилання на документацію (таблиця вгорі сторінки): Документація Node Найважливіше, що потрібно запам'ятати:
  1. Атрибути є ТІЛЬКИ у елементів.
  2. У елементів немає значення.
  3. Ім'я вузла-елемента збігається з іменем тега, а вузла-атрибута з іменем атрибута.
<h2>DOM (Document Object Model) – ПРАКТИКА</h2>У практичній частині ми розбиратимемо різного роду завдання пошуку інформації в XML. Також взято дві задачі з минулої статті для порівняння зручності. Давайте починати, а почати добре було б із імпортів:
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
Даю імпорти, щоб ви не сплутали класи :) Завдання №1 – нам потрібно дістати інформацію про всіх співробітників та вивести її в консоль з наступного XML файлу:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<company>
    <name>IT-Heaven</name>
    <offices>
        <office floor="1" room="1">
            <employees>
                <employee name="Maksim" job="Middle Software Developer" />
                <employee name="Ivan" job="Junior Software Developer" />
                <employee name="Franklin" job="Junior Software Developer" />
            </employees>
        </office>
        <office floor="1" room="2">
            <employees>
                <employee name="Herald" job="Middle Software Developer" />
                <employee name="Adam" job="Middle Software Developer" />
                <employee name="Leroy" job="Junior Software Developer" />
            </employees>
        </office>
    </offices>
</company>
Як ми бачимо, у нас вся інформація збережена в елементах employee. Для того, щоб десь зберігати її у нас у програмі, давайте створимо клас Employee:
public class Employee {
    private String name, job;

    public Employee(String name, String job) {
        this.name = name;
        this.job = job;
    }

    public String getName() {
        return name;
    }

    public String getJob() {
        return job;
    }
}
Тепер, коли ми маємо опис структури для зберігання даних, нам потрібна колекція, яка буде зберігати співробітників. Її ми створюватимемо в самому коді. Також нам потрібно створити Document на основі нашого XML:
public class DOMExample {
    // Список для сотрудников из XML файлу
    private static ArrayList<Employee> employees = new ArrayList<>();

    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        // Получение фабрики, чтобы после получить билдер документов.
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        // Получабо из фабрики билдер, который парсит XML, создает структуру Document в виде иерархического дерева.
        DocumentBuilder builder = factory.newDocumentBuilder();

        // Запарсабо XML, создав структуру Document. Теперь у нас есть доступ ко всем елементам, яким нам нужно.
        Document document = builder.parse(new File("resource/xml_file1.xml"));
    }
}
Після отримання документа, ми маємо необмежену владу над усією структурою XML файлу. Ми можемо отримувати будь-які елементи в будь-який час, повертатися назад, щоб перевірити будь-які дані і, в цілому, більш гнучкий підхід, ніж у нас в SAX. У контексті даного завдання нам потрібно просто витягти всі елементи працівника, а потім отримати всю інформацію про них. Це досить просто:
public class DOMExample {
    // Список для сотрудников из XML файлу
    private static ArrayList<Employee> employees = new ArrayList<>();

    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        // Получение фабрики, чтобы после получить билдер документов.
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        // Получабо из фабрики билдер, который парсит XML, создает структуру Document в виде иерархического дерева.
        DocumentBuilder builder = factory.newDocumentBuilder();

        // Запарсабо XML, создав структуру Document. Теперь у нас есть доступ ко всем елементам, яким нам нужно.
        Document document = builder.parse(new File("resource/xml_file1.xml"));

        // Получение списка всех элементов employee внутри корневого елемента (getDocumentElement возвращает ROOT элемент XML файлу).
        NodeList employeeElements = document.getDocumentElement().getElementsByTagName("employee");

        // Перебор всех элементов employee
        for (int i = 0; i < employeeElements.getLength(); i++) {
            Node employee = employeeElements.item(i);

            // Получение атрибутов каждого елемента
            NamedNodeMap attributes = employee.getAttributes();

            // Добавление сотрудника. Атрибут - тоже Node, потому нам нужно получить значення атрибута с помощью метода getNodeValue()
            employees.add(new Employee(attributes.getNamedItem("name").getNodeValue(), attributes.getNamedItem("job").getNodeValue()));
        }

        // Вывод информации о каждом сотруднике
        for (Employee employee : employees)
            System.out.println(String.format("Информации о сотруднике: ім'я - %s, должность - %s.", employee.getName(), employee.getJob()));
    }
}
Опис цього рішення прямо у рішенні. Бажано після перегляду коду, повернутися назад до теорії та прочитати ще раз. Насправді все зрозуміло інстинктивно. Прочитайте уважно коментарі та питань бути не повинно, а якщо залишабося – можете написати в коментарях, я відповім, або в личку, або просто запустити свою ІДЕЮ і самому спробувати погратися з кодом, якщо ви цього ще не зробабо. Таким чином, після запуску коду ми отримали наступні вихідні дані:
Информации о сотруднике: ім'я - Maksim, должность - Middle Software Developer.
Информации о сотруднике: ім'я - Ivan, должность - Junior Software Developer.
Информации о сотруднике: ім'я - Franklin, должность - Junior Software Developer.
Информации о сотруднике: ім'я - Herald, должность - Middle Software Developer.
Информации о сотруднике: ім'я - Adam, должность - Middle Software Developer.
Информации о сотруднике: ім'я - Leroy, должность - Junior Software Developer.
Як можна помітити, завдання успішно виконане! Давайте переходити до наступного завдання :) Завдання №2 – вводиться з консолі ім'я елемента, про який потрібно вивести інформацію про всі елементи всередині його та їх атрибути з наступного XML файлу:
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <oracle>
        <connection value="jdbc:oracle:thin:@10.220.140.48:1521:test1" />
        <user value="secretOracleUsername" />
        <password value="111" />
    </oracle>

    <mysql>
        <connection value="jdbc:mysql:thin:@10.220.140.48:1521:test1" />
        <user value="secretMySQLUsername" />
        <password value="222" />
    </mysql>
</root>
Все досить просто: ми повинні отримати елемент за його ім'ям, яке вважаємо, а потім пройти всіма дочірніми вузлами. Для цього потрібно пройтися всіма дочірніми вузлами всіх дочірніх вузлів, які є елементами. Розв'язання цієї задачі:
public class DOMExample {
    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        // Ридер для считывания имени тега из консоли
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

        // Получение фабрики, чтобы после получить билдер документов.
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        // Получабо из фабрики билдер, который парсит XML, создает структуру Document в виде иерархического дерева.
        DocumentBuilder builder = factory.newDocumentBuilder();

        // Запарсабо XML, создав структуру Document. Теперь у нас есть доступ ко всем елементам, яким нам нужно.
        Document document = builder.parse(new File("resource/xml_file3.xml"));

        // Считывание имени тега для поиска его в файле
        String element = reader.readLine();

        // Получение списка элементов, однако для удобства будем рассматривать только первое совпадение в документі.
        // Так же заметьте, что мы ищем элемент внутри документа, а не рут елемента. Это сделано для того, чтобы рут элемент тоже искался.
        NodeList matchedElementsList = document.getElementsByTagName(element);

        // Даже если елемента нет, всегда будет возвращаться список, просто он будет пустым.
        // Потому, чтобы утверждать, что елемента нет в файле, достаточно проверить размер списка.
        if (matchedElementsList.getLength() == 0) {
            System.out.println("Тег " + element + " не был найден в файле.");
        } else {
            // Получение первого елемента.
            Node foundedElement = matchedElementsList.item(0);

            System.out.println("Элемент был найден!");

            // Если есть данные внутри, вызов метода для вывода всей информации
            if (foundedElement.hasChildNodes())
                printInfoAboutAllChildNodes(foundedElement.getChildNodes());
        }
    }

    /**
     * Рекурсивный метод, который будет выводить информацию про все узлы внутри всех узлов, которые пришли параметром, пока не будут перебраны все узлы.
     * @param list Список узлов.
     */
    private static void printInfoAboutAllChildNodes(NodeList list) {
        for (int i = 0; i < list.getLength(); i++) {
            Node node = list.item(i);

            // У элементов есть два вида узлов - другие элементы або текстовая информация. Потому нужно разбираться две ситуации отдельно.
            if (node.getNodeType() == Node.TEXT_NODE) {
                // Фильтрация информации, так як пробелы и переносы строчек нам не нужны. Это не информация.
                String textInformation = node.getNodeValue().replace("\n", "").trim();

                if(!textInformation.isEmpty())
                    System.out.println("Внутри елемента найден текст: " + node.getNodeValue());
            }
            // Если это не текст, а элемент, то обрабатываем его як элемент.
            else {
                System.out.println("Найден элемент: " + node.getNodeName() + ", его атрибуты:");

                // Получение атрибутов
                NamedNodeMap attributes = node.getAttributes();

                // Вывод информации про все атрибуты
                for (int k = 0; k < attributes.getLength(); k++)
                    System.out.println("Ім'я атрибута: " + attributes.item(k).getNodeName() + ", его значення: " + attributes.item(k).getNodeValue());
            }

            // Если у данного елемента еще остались узлы, то вывести всю информацию про все его узлы.
            if (node.hasChildNodes())
                printInfoAboutAllChildNodes(node.getChildNodes());
        }
    }
}
Всі описи рішення є в коментарях, однак хотілося б трохи зобразити графічно підхід, який ми задіяли, на прикладі картинки з теорії. Вважатимемо, що інформацію нам потрібно вивести про тег html. Як ви бачите, нам потрібно йти згори донизу від кореня дерева. Усі лінії – це вузли. У рішенні ми рекурсивно йтимемо від початку потрібного елемента всіма його вузлами, і якщо якийсь з його вузлів – елемент, то ми перебираємо так само всі вузли цього елемента. Таким чином, після запуску коду ми отримали наступні вихідні дані для root:
Элемент был найден!
Найден элемент: oracle, его атрибуты:
Найден элемент: connection, его атрибуты:
Ім'я атрибута: value, его значення: jdbc:oracle:thin:@10.220.140.48:1521:test1
Найден элемент: user, его атрибуты:
Ім'я атрибута: value, его значення: secretOracleUsername
Найден элемент: password, его атрибуты:
Ім'я атрибута: value, его значення: 111
Найден элемент: mysql, его атрибуты:
Найден элемент: connection, его атрибуты:
Ім'я атрибута: value, его значення: jdbc:mysql:thin:@10.220.140.48:1521:test1
Найден элемент: user, его атрибуты:
Ім'я атрибута: value, его значення: secretMySQLUsername
Найден элемент: password, его атрибуты:
Ім'я атрибута: value, его значення: 222
Завдання успішно вирішено! Завдання №3 – з наступного XML файлу, де збережена інформація про студентів, професорів та співробітників, потрібно рахувати інформацію та вивести її в консоль:
<?xml version="1.0" encoding="UTF-8"?>
<database>
    <students>
        <student name="Maksim" course="3" specialization="CE" />
        <student name="Stephan" course="1" specialization="CS" />
        <student name="Irvin" course="2" specialization="CE" />
    </students>

    <professors>
        <professor name="Herald" experience="7 years in University" discipline="Math" />
        <professor name="Adam" experience="4 years in University" discipline="Programming" />
        <professor name="Anton" experience="6 years in University" discipline="English" />
    </professors>

    <service>
        <member name="John" position="janitor" />
        <member name="Jordan" position="janitor" />
        <member name="Mike" position="janitor" />
    </service>
</database>
Завдання досить просте, проте цікаве. Для початку, нам потрібно створити 4 класи: співробітника, професора та студента, а також загальний абстрактний клас Human, щоб винести змінну name з кожного класу під загальний знаменник: Абстрактний клас-батько
public abstract class Human {
    private String name;

    public Human(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
Студент
public class Student extends Human {
    private String course, specialization;

    public Student(String name, String course, String specialization) {
        super(name);
        this.course = course;
        this.specialization = specialization;
    }

    public String getCourse() {
        return course;
    }

    public String getSpecialization() {
        return specialization;
    }

    public String toString() {
        return "Голодный студент " + getName() + " " + course + "-го курса, обучающийся по специальности " + specialization;
    }
}
Професор
public class Professor extends Human {
    private String experience, discipline;

    public Professor(String name, String experience, String discipline) {
        super(name);
        this.experience = experience;
        this.discipline = discipline;
    }

    public String getExperience() {
        return experience;
    }

    public String getDiscipline() {
        return discipline;
    }

    public String toString() {
        return "Профессор " + getName() + ", обладающий опытом: \"" + experience + "\", выкладает дисциплину " + discipline;
    }
}
Співробітник
public class Member extends Human {
    private String position;

    public Member(String name, String position) {
        super(name);
        this.position = position;
    }

    public String getPosition() {
        return position;
    }

    public String toString() {
        return "Сотрудник обслуживающего персонала " + getName() + ", должность: " + position;
    }
}
Тепер, коли наші класи готові, нам достатньо написати код для отримання всіх елементів student, professor та member, а потім дістати їх атрибути. Для зберігання ми використовуватимемо колекцію, яка зберігатиме об'єкти спільного для всіх батьківського класу – Human. Отже, розв'язання цього завдання:
public class DOMExample {
    // Коллекция для хранения всех людей
    private static ArrayList<Human> humans = new ArrayList<>();

    // Константы для элементов
    private static final String PROFESSOR = "professor";
    private static final String MEMBER = "member";
    private static final String STUDENT = "student";

    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        // Получение фабрики, чтобы после получить билдер документов.
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        // Получабо из фабрики билдер, который парсит XML, создает структуру Document в виде иерархического дерева.
        DocumentBuilder builder = factory.newDocumentBuilder();

        // Запарсабо XML, создав структуру Document. Теперь у нас есть доступ ко всем елементам, яким нам нужно.
        Document document = builder.parse(new File("resource/xml_file3.xml"));

        // Получение информации про каждый элемент отдельно
        collectInformation(document, PROFESSOR);
        collectInformation(document, MEMBER);
        collectInformation(document, STUDENT);

        // Вывод информации
        humans.forEach(System.out::println);
    }

    /**
     * Метод ищет информацию про теги по имени element и вносит всю информацию в коллекцию humans.
     * @param document Документ, в котором будем искать элементы.
     * @param element Ім'я елемента, теги которого нужно найти. Должна быть одна из констант, которые определяются выше.
     */
    private static void collectInformation(Document document, final String element) {
        // Получение всех элементов по имени тега.
        NodeList elements = document.getElementsByTagName(element);

        // Перебор всех найденных элементов
        for (int i = 0; i < elements.getLength(); i++) {
            // Получение всех атрибутов елемента
            NamedNodeMap attributes = elements.item(i).getAttributes();
            String name = attributes.getNamedItem("name").getNodeValue();

            // В зависимости от типа елемента, нам нужно собрать свою дополнительну информацию про каждый подкласс, а после добавить нужные образцы в коллекцию.
            switch (element) {
                case PROFESSOR: {
                    String experience = attributes.getNamedItem("experience").getNodeValue();
                    String discipline = attributes.getNamedItem("discipline").getNodeValue();

                    humans.add(new Professor(name, experience, discipline));
                } break;
                case STUDENT: {
                    String course = attributes.getNamedItem("course").getNodeValue();
                    String specialization = attributes.getNamedItem("specialization").getNodeValue();

                    humans.add(new Student(name, course, specialization));
                } break;
                case MEMBER: {
                    String position = attributes.getNamedItem("position").getNodeValue();

                    humans.add(new Member(name, position));
                } break;
            }
        }
    }
}
Зауважте, що нам достатньо лише назви елемента, щоб отримати взагалі всі ці елементи з документа. Це значно полегшує процес пошуку потрібної інформації. Вся інформація про код розміщена в коментарі. Нового нічого не було використано, чого не було б у попередніх завданнях. Вихідні дані коду:
Профессор Herald, обладающий опытом: "7 years in University", выкладает дисциплину Math
Профессор Adam, обладающий опытом: "4 years in University", выкладает дисциплину Programming
Профессор Anton, обладающий опытом: "6 years in University", выкладает дисциплину English
Сотрудник обслуживающего персонала John, должность: janitor
Сотрудник обслуживающего персонала Jordan, должность: janitor
Сотрудник обслуживающего персонала Mike, должность: janitor
Голодный студент Maksim 3-го курса, обучающийся по специальности CE
Голодный студент Stephan 1-го курса, обучающийся по специальности CS
Голодный студент Irvin 2-го курса, обучающийся по специальности CE
Завдання вирішено! Рекомендації, коли використовувати DOM, а коли SAX Різниця між цими інструментами у функціональності та швидкості роботи. Якщо вам потрібен більш гнучкий функціонал і ви можете дозволити собі витрачати продуктивність програми, то ваш вибір - DOM, якщо ваша головна мета - скорочення витрат пам'яті, то DOM не найкращий вибір, так як він зчитує всю інформацію з XML файлу і зберігає її. Тому, спосіб SAX послідовного зчитування менш витратний. Коротко: якщо потрібна продуктивність SAX, функціональність DOM. <h2>Висновок</h2>У кожного програміста є свої інструменти, і, залежно від завдання, потрібно використовувати ті чи інші інструменти. У статтях про SAX і DOM я ставив за мету навчити вас витягувати інформацію з XML файлів і обробляти їх так, як вам це потрібно. Однак, навіть якщо ви прочитали ці статті, ви не можете стверджувати, що навчабося використовувати ці інструменти. Ви повинні попрактикуватися, протестувати код із статей, розібратися в його роботі та спробувати самим щось написати. Як ніяк, найважливіше – практика. Остання стаття буде найближчим часом і, мабуть, вже після закінчення терміну конкурсу, і буде присвячена JAXB. JAXB – інструмент для збереження об'єктів у вашій програмі у форматі XML. На цьому все, сподіваюся, що ця стаття була корисною, і успіхів вам у програмуванні :) Попередня стаття:[Конкурс] Основи XML для Java програміста - Частина 3.1 із 3 - SAX
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ