11 марта 2011 г.

JDOM: работа с XML на Java - проще простого

JDOM – это максимально упрощенная open source Java-библиотека для создания, парсинга, изменения и сериализации XML-документов. В отличие от других подобных DOM APIs, JDOM ориентированна на язык Java, а не на спецификации XML, что очень упрощает код и облегчает его интуитивное понимание. При этом JDOM не является "велосипедом" и не содержит в себе свой собственный парсер, а использует уже существующие наработки – стандартные пакеты javax.xml и org.xml.sax. Если вам понадобилось работать с XML, то обратите сначала внимание именно на эту библиотеку, скорее всего вам она понравится.

Краткое описание модели библиотеки JDOM

Так же, как и сам XML, JDOM APIs являются древовидными. Для представления XML в библиотеке, главным образом, используются классы Document, Element и Attribute.

Документ XML в JDOM представляет из себя дерево, корнем которого является объект класса Document. Экземпляр Document является контейнером для всех остальных элементов и обязательно содержит один корневой элемент (экземпляр Element). Главной характеристикой объекта Element является его содержание (Content) и список атрибутов (Attribute). Класс Content является родительским для классов Element, Comment и Text (а так же для некоторых других), поэтому экземпляры перечисленных классов могут быть содержимым объекта Element. Объект Element есть ничто иное как XML-тег, у которого обязательно должно быть имя и могут быть атрибуты. Классы Comment и Text соответственно представляют комментарий и текст внутри тега. Экземпляры Attribute, Comment и Text не могут содержать внутри себя никаких элементов и по сути являются объектами, которые содержат только текстовые значения. Значение экземпляра Attribute может быть получено по имени в отличие от Comment и Text, у которых имени нет.

Для того что бы все стало совсем понятно, приведу схему документа, который ниже будет создан и прочитан с помощью JDOM.


Представленный документ является списком глав подразделений компании. Документ для простоты содержит всего два руководителя (<head>). У каждого элемента <head> есть атрибут id, а в качестве содержания – комментарий, кратко описывающий руководителя и элементы <name> и <department>, которые в свою очередь содержат текст, представляющий имя руководителя и название подразделения соответственно.

Теперь посмотрим, как будет выглядеть сам XML.
<?xml version="1.0" encoding="UTF-8"?>
<Head_list>
  <head id="1">
    <!--Head of IT department-->
    <name>Petrov P.</name>
    <department>IT</department>
  </head>
  <head id="2">
    <!--Head of Sales-->
    <name>Sidorov S.</name>
    <department>Sales</department>
  </head>
</Head_list>

Создание XML-документа

Для того чтобы создать XML-документ в общем случае необходимо выполнить следующие этапы:
  1. Создать экземпляр Document
  2. Создать корневой Element и добавить его в экземпляр Document
  3. Создать и заполнить, начиная с корневого элемента, необходимое дерево с помощью экземпляров Element, Attribute, Comment и т.д.
  4. Если необходимо, вывести (например в файл) созданный документ как поток байт с помощью класса XMLOutputter.
Пример метода, который создаёт описанное выше XML-дерево и выводит его на стандартный вывод, а так же в файл Heads.xml.

private static void createXMLDocument(){
      
        // Создаем документ
        Document xmlDoc = new Document();
        // Создаем корневой элемент
        Element root = new Element("Head_list");
        // Добавляем корневой элемент в документ
        xmlDoc.setRootElement(root);
        
        // Создаем элемент head и добавляем ему атрибут
        Element head1 = new Element("head");
        head1.setAttribute("id", "1");
        // Добавляем комментарий в контент элемента head
        head1.addContent(new Comment("Head of IT department"));

        // Создаем элемент name и добавляем в него текст
        Element name1 = new Element("name");
        name1.addContent("Petrov P.");
          
        // Добавляем элемент name в элемент head
        head1.addContent(name1);
          
        // Создаем элемент department
        Element department1 = new Element("department");
        department1.addContent("IT");
        head1.addContent(department1);
          
        // Добавляем элемент head в корневой элемент
        root.addContent(head1);
      
        // Еще один элемент head создадим более кратко
        root.addContent(new Element("head").setAttribute("id","2")
                        .addContent(new Comment("Head of Sales"))
                            .addContent(new Element("name").addContent("Sidorov S."))
                            .addContent(new Element("department").addContent("Sales"))
                         );
       
        try {
            // Получаем "красивый" формат для вывода XML
            // с переводами на новую строку и отступами
            Format fmt = Format.getPrettyFormat();  

            // Выводим созданный XML как поток байт на стандартный
            // вывод и в файл, используя подготовленный формат
            XMLOutputter serializer = new XMLOutputter(fmt);
            serializer.output(xmlDoc, System.out);
            serializer.output(xmlDoc, new FileOutputStream(new File("Heads.xml")));
        }
        catch (IOException e) {
            System.err.println(e);
        }
  }

Из комментариев весь код должен быть понятен, но все же сделаю пару пояснений.

Метод addContent() после выполнения возвращает ссылку на тот же объект из которого он был вызван, благодаря чему можно создавать целую цепочку вызовов addContent(), заполняя содержание всех элементов очень небольшим объемом кода. Однако, не стоит этим сильно увлекаться, что бы код оставался понятным.

Форматом вывода можно управлять с помощью класса org.jdom.output.Format. Например, можно установить отступ, символ новой строки, кодировку вывода, указать как следует выводить пустые элементы (<empty/> или <empty></empty>), установить автоматическое удаление лишних пробельных символов и т.д..

Разбор XML-файла

Работа с существующим XML-файлом состоит из следующих этапов:
  1. Создание экземпляра класса org.jdom.input.SAXBuilder, который умеет строить JDOM-дерево из файлов, потоков, URL и т.д.
  2. Вызов метода build() экземпляра SAXBuilder с указанием файла или другого источника.
  3. Навигация по дереву и манипулирование элементами, если это необходимо.
С первыми двумя пунктами все предельно ясно, а вот навигация по дереву может осуществляться множеством способов.

Для того, чтобы пробежаться по всем элементам дерева (экземпляры Element), можно воспользоваться методом getChildren(), что бы получить список List всех дочерних экземпляров корневого Element, а затем для каждого дочернего элемента так же вызвать getChildren() и т.д.

Пример использования getChildren()
public static void listChildren(Element element) {
    System.out.println(element.getName());
    List children = element.getChildren();
    Iterator iterator = children.iterator();
    while (iterator.hasNext()) {
      Element child = (Element) iterator.next();
      listChildren(child);
    } 
}

Можно поступить по-другому – использовать методы getContent() и getDescendants(), передавая им фильтры для того что бы ограничить выборку.

Метод getContent() возвращает список List вложенных элементов, причем поиск осуществляется только на глубине 1 уровня. Таким образом, с помощью getContent() можно получить дочерние элементы объекта у которого вызывается данный метод, но нельзя получить дочерние элементы его дочерних элементов.

Метод getDescendants() возвращает Iterator по всем найденным элементам, но в отличие от getContent() поиск ведется на всех уровнях вложенности элементов.

Как я уже сказал, для ограничения выборки дочерних элементов в обоих методах можно использовать фильтры. Фильтры это классы, которые реализуют интерфейс org.jdom.filter.Filter. В библиотеке JDOM есть два готовых класса фильтра, реализующих этот интерфейс: org.jdom.filter.ContentFilter и org.jdom.filter.ElementFilter. Используя фильтр ElementFilter, можно осуществлять поиск объектов Element ограничивая его именем элемента и/или пространством имен (Namespace), а с помощью ContentFilter можно ограничивать поиск определенным "видом" элемента, например, получить все комментарии и текст, которые содержит элемент или только текст и в таком духе. В тоже время интерфейс Filter очень прост и требует реализации лишь одного метода matches(java.lang.Object obj), который возвращает true в случае если элемент obj соответствует заданным параметрам фильтра, поэтому довольно легко можно создавать свои изощеренные фильтры.

Теперь приведу пример метода, который парсит созданный ранее XML-файл.
private static void readXMLDocument() {
      SAXBuilder parser = new SAXBuilder();
      Document xmlDoc;
      
      try {
        xmlDoc = parser.build(new File("Heads.xml"));

        System.out.println("Heads:");
        
        // Получаем список всех элементов head, которые 
        // содержит корневой элемент 
        List elements = xmlDoc.getRootElement()
                            .getContent(new ElementFilter("head"));      
        
        // Для каждого элемента head получаем значение атрибута
        // id и текст вложенных элементов name и department
        Iterator iterator = elements.iterator();
        while(iterator.hasNext()){          
            Element head = (Element)iterator.next();
            String id = head.getAttributeValue("id");           
            String name = head.getChildText("name");
            String department = head.getChildText("department");
         
            System.out.println(id+": "+name+" - "+department);
        }
        
        System.out.println("Comments:");
        
        // Получаем все комментарии в документе и выводим для 
        // каждого его значение и имя элемента, который содержит
        // этот комментарий
        iterator = xmlDoc.getDescendants(new ContentFilter(ContentFilter.COMMENT));
        while(iterator.hasNext()){    
            Content comment = (Content)iterator.next();
            System.out.println(comment.getParentElement().getName()+": "+ comment.getValue());
        }
      } catch (JDOMException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      }
  }
После выполнения метода на консоль будет выведено следующее:

Heads:
1: Петров - IT
2: Sidorov S. - Sales
Comments:
head: Head of IT department
head: Head of Sales

Изменение дерева осуществляется с помощью различных методов вроде removeChild(), removeChildren(), removeContent(), addContent(), setContent(), removeAttribute(), setAttribute(), setName(), setText() и т.д. и т.п., думаю что это и так понятно.

Более подробную информацию о JDOM можно найти на официальном сайте, а так же в главах 14 и 15 свободно доступной книги о работе с XML на Java (всё на английском).

7 комментариев:

  1. Это прекрасная статья. Не знал, до прочтения, как начать работать с применением JDOM и XML.

    ОтветитьУдалить
    Ответы
    1. Рад, что пригодилась. Спасибо за добрый комментарий :)

      Удалить
  2. Спасибо. Помогло. очень подробно

    ОтветитьУдалить
  3. Мега большое спасибище!!! коротко и ясно !!

    ОтветитьУдалить
  4. Добрый день! Есть задача нагенерить огромный xml (5 млн элементов). Получается нагенерить лишь около 500 000, далее outOfMemory или утилизируется полностью heap (2 ГБ). Как обойти этот момент?

    ОтветитьУдалить