КаталогИндекс раздела


© IBM deweloperWork
© А.С.Деревянко (перевод)

XML-программирование в технологии Java. Часть 1

Doug Tidwell

13 января 2004

В этом измененном учебнике рассматриваются основные принципы манипулирования XML-документами при помощи технологии Java. Doug Tidwell рассматривает общие API для XML и обсуждает, как разбирать, создавать, манипулировать и преобразовывать XML-документы.

Содержание

  1. Введение
  2. Основы парсеров
  3. Объектная Модель Документа (DOM)
  4. SAX: Simple API for XML
  5. JDOM
  6. Резюме
  7. Ресурсы
  8. Об авторе

 

Раздел 1. Введение

Об этом учебнике

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

Наиболее общей задачей обработки XML является разбор (parsing) XML-документа. Разбор включает в себя чтение XML-документа для определения его структуры и содержимого. Одним из удобств программирования в XML является доступность бесплатных XML-парсеров с открытым кодом, которые читают для вас XML-документы. Этот учебник подробно рассматривает создание объектов парсеров, приказание этим парсерам прочитать XML-файлы и обработку результатов. Как вы можете ожидать, вы можете выполнить эти общие задачи несколькими разными способами; я буду рассматривать стандарты, привлекаемые при использовании вами того или иного подхода.

Программные интерфейсы

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

Первые три из этих интерфейсов (DOM, SAX и JDOM) определяют, как представляется содержимое XML-документа и как происходит доступ к нему. JAXP содержит классы для создания объектов парсеров. Чтобы создать парсеры DOM или SAX, вы используете JAXP. Когда вы используете JDOM, библиотека JDOM использует "под ковром" JAXP для создания парсера. В итоге:

Я буду объяснять цели создания, достоинства и недостатки каждого из этих API, вместе с некоторой информацией об их истории и об организациях, которые их создали.

О примерах

Через весь этот учебник я буду показывать вам примеры программ, которые используют API DOM, SAX и JDOM. Все они работают с размеченным в XML сонетом Шекспира. Структура сонета такова:

<sonnet>
 <author>
  <lastName>
  <firstName>
  <nationality>
  <yearOfBirth>
  <yearOfDeath>
  </author>
 <lines>
  [14 элементов <line>]
 </lines>
</sonnet>

Законченный пример см. в sonnet.xml и sonnet.dtd.

Установка вашей машины

Вам следует установить несколько вещей на вашей машине прежде, чем вы сможете выполнять примеры. (Предполагается, что вы знаете, как компилировать и запускать Java-программы, и что вы знаете, как установить переменную CLASSPATH.)

  1. Прежде всего, зайдите на домашнюю страницу XML-парсера Xerces в проекте Apache XML (http://xml.apache.org/xerces2-j/). Вы можете сразу пойти на страницу выгрузки (http://xml.apache.org/xerces2-j/download.cgi).
  2. Распакуйте файл, который вы выгрузите с Apache. При этом создастся каталог с именем xerces-2_5_0 или как-то в этом роде, в зависимости от релиза парсера. JAR-файлы, которые вам нужны (xercesImpl.jar и xml-apis.jar) должны быть в корневом каталоге Xerces.
  3. Зайдите на Web-сайт проекта JDOM и выгрузите последнюю версию JDOM (http://jdom.org/).
  4. Распакуйте файл, который вы выгрузите с JDOM. При этом создастся каталог с именем jdom-b9 или как-то в этом роде. JAR-файл, который вам нужен (jdom.jar) должен быть в каталоге build.
  5. Наконец, выгрузите архив с примерами для этого учебника, xmlprogj.zip, и распакуйте файл.
  6. Добавьте текущий каталог (.), xercesImpl.jar, xml-apis.jar и jdom.jar в вашу переменную CLASSPATH.

 

Раздел 2. Основы парсеров

Основы

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

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

Как использовать парсер

Я расскажу об этом более подробно в следующих разделах, но в общих чертах, вы используете парсер так:

  1. Создаете объект парсера
  2. Указываете объекту парсера на ваш XML-документ
  3. Обрабатываете результаты

Очевидно, что третий шаг является сложным. Если вы знаете содержимое XML-документа, вы можете захотеть, например, генерировать Web-страницу, создать заказ на покупку или построить секторную диаграмму. Учитывая неограниченный объем данных, который может содержаться в XML-документе, задача написания приложения, которое будет корректно обрабатывать все возможные варианты входных данных, представляется ужасающей. К счастью, общие инструменты разбора XML, обсуждаемые здесь, делают эту задачу гораздо более простой.

Виды парсеров

Парсеры классифицируются несколькими различными способами:

Проверяющие и непроверяющие парсеры

Для тех из вас, кто новичок в XML, скажем, что есть три разных вида XML-документов:

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

Почему используются непроверяющие парсеры?

Так почему же используются непроверяющие парсеры? So why use a non-validating парсер? Две причины: скорость и эффективность. От XML-парсер требуются некоторые затраты на чтение DTD или схемы, а затем установку движка проверки правил, который обрабатывает каждый элемент и атрибут в XML-документе в соответствии с правилами. Если вы уверены, что XML-документ правильный (Например, если он генерируется из запроса к базе данных), вы можете пропустить проверку правильности. В зависимости от сложности правил для документа, это может сэкономить значительный объем времени и памяти.

Если же у вас хрупкий код, который получает входные данные из XML-документов, и этот код требует, чтобы документы следовали определенному DTD или схеме, вам, возможно, следует проверять все, независимо от того, как дорого или долго это будет.

Парсеры которые поддерживают языки схемы XML

Исходная спецификация XML определила Определение Типа Документа (DTD) как способ определения того, как должен выглядеть правильный XML-документ. Большая часть синтаксиса DTD происходила из SGML и рассматривала проверку правильности с точки зрения публикации, а не с точки зрения типизации данных. Кроме того, DTD имеют синтаксис, отличный от XML-документов, что делает трудным для пользователей понимание синтаксиса правил документа.

Чтобы преодолеть эти ограничения, Worldwide Web Consortium (W3C) создал язык схемы XML. Схема XML позволяет вам определить, как выглядит правильный документ, с гораздо большей точностью. Язык схемы XML очень богат, а документы схемы XML Schema могут быть очень сложными. По этой причине XML-парсеры, которые поддерживают проверку правильности при помощи схемы XML имеют тенденцию быть очень большими. Знайте также, что язык схемы XML W3C не является единственным игроком на этом поле. Некоторые парсеры поддерживают другие языки схемы XML, такие как RELAX NG или Schematron (Я буду обсуждать языки схемы позднее в этом учебнике.)

Объектная Модель Документа (DOM)

Объектная Модель Документа или DOM является официальной рекомендацией W3C. Она определяет доступный для программ интерфейс доступа и изменения структуры XML-документов. Когда XML-парсер заявляет, что он поддерживает DOM, это означает, что он реализует интерфейсы, определенные в стандарте.

В настоящий момент два уровня DOM являются официальными рекомендациями, названные DOM Уровня 1 и DOM Уровня 2. Ожидается, что DOM Уровня 3 станет официальной рекомендацией в начале 2004. Все функции DOM, обсуждаемые в данном учебнике, являются частью DOM Уровня 1, так что кодовые примеры будут работать с любым парсером DOM.

Что вы получаете от парсера DOM

Когда вы разбираете XML-документ при помощи парсера DOM, вы получаете структуру дерева, которая представляет содержимое XML-документа. Весь текст, элементы и атрибуты (вместе с другими вещами, которые я вкратце рассмотрю) находятся в структуре дерева. DOM также предоставляет различные функции, которые вы можете использовать для исследования содержимого и структуры дерева и манипулирования ими.

Знайте, что парсер не отслеживает некоторых вещей. Например, количества пропусков между атрибутами и их значения не сохраняются в дереве DOM. Ч точки зрения парсеров это три атрибута совершенно одинаковы:

DOM не дает вам никакого способа узнать, как был кодирован исходный документ. Другой пример, - XML парсер не сообщит вам, был ли исходный элемент закодирован одним тегом или двумя. (Например, была ли горизонтальная линия XHTML закодирована как <hr/> или как <hr></hr>? ) Информация, которую должен отслеживать XML-парсер, называется XML Infoset (см. Ресурсы).

Simple API for XML (SAX)

Simple API for XML (SAX) является альтернативой для работы с содержимым XML-документов. Он разработан с меньшими требованиями к объему памяти, но он перекладывает больше работы на программиста. SAX и DOM являются комплиментарными, каждый из них лучше соответствует разным ситуациям.

Являясь стандартом де-факто, SAX был исходно разработан David Megginson при участии многих пользователей через Internet. См. полный стандарт SAX в Ресурсы. Документация вашего парсера, возможно, также описывает стандарт SAX.

Что вы получаете от парсера SAX

Когда вы разбираете XML-документ при помощи парсера SAX, парсер генерирует серию событий по мере того, как он читает документ. На ваше усмотрение - что делать по этим событиям. Вот несколько примеров событий, которые вы можете получить при разборе XML-документа:

Как и в DOM, парсер SAX не показывает некоторые подробности, например, порядок появления атрибутов.

JDOM

Хотя SAX и DOM обеспечивают много полезных функций, некоторые задачи являются более сложными, чем хотелось бы разработчикам. В соответствии с традицией сообщества открытых кодов - создавать новые средства, когда в них существует необходимость, - специалисты в технологии Java Jason Hunter и Brett McLaughlin создали JDOM, библиотеку Java, которая значительно упрощает работу с XML-документами. Как и DOM, JDOM обеспечивает дерево объектов, которое представляет XML-документ, но способ работы с этими объектами значительно лучше интуитивно понятен для программистов Java. Имейте в виду, что JDOM включает в себя адаптеры для использования на заднем плане обычных парсеров SAX и DOM; JDOM обеспечивает адаптеры для всех главных (и некоторых второстепенных) XML-парсеров Java, так что вам не нужно беспокоиться о том, поддерживает ли ваш Java XML-парсер JDOM или нет. JDOM использует парсер на заднем плане без какого-либо вашего вмешательства.

Как выбрать парсер

Я расскажу об этом подробнее позже, но в общем случае, вы должны использовать парсер DOM, когда:

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

Наконец, взглянем на JDOM API. Использование памяти в JDOM меньшее, чем в DOM, но не такое хорошее, как в SAX. Также, если вы хотите выполнять проверку правильности (тема, которую я не обсуждаю в данном учебнике), JDOM требует, чтобы вы конфигурировали работающий на заднем плане парсер; JDOM сама не выполняет проверки правильности. Можно сказать, что если JDOM делает все, что вам нужно, и работает достаточно быстро для вас, она упростит вам кодирование.

 

Раздел 3. Объектная Модель Документа (DOM)

Общий интерфейс для структур XML

DOM - это интерфейс для работы со структурой XML-документов. DOM, проект W3C былв разработана для предоставления ряда объектов и методов, призванных облегчить жизнь программистов. (Технически DOM предшествовалаXML; ранняя работа по DOM была предназначена для HTML-документов.) В идеали вы должны быть в состоянии написать программу, которая использует один DOM-совместимый парсер для обработки process XML-документа, а затем переключиться на другой DOM-совместимый парсер без изменения вашего кода.

Когда вы разбираете XML-документ при помощи парсер DOM, вы получаете иерархическую структуру данных (дерево DOM), которая представляет все, что парсер нашел в XML-документе. Вы можете затем использовать функции DOM, чтобы манипулировать деревом, перемещать ветви, добавлять новые ветви, удалять части дерева.

Как я отмечал выше, некоторая информация недоступна от XML-парсера. Отдельные парсеры могут реализовывать функции, которые дают вам возможность получить большую информацию, об исходном документе, но эти функции не являются частью DOM, и не захотят работать с любым другим парсером. См. Дополнительную информацию в стандарте на XML Infoset (в Ресурсы).

Знайте ваши узлы

Вы уже знаете, что парсер DOM возвращает вам структуру дерева; это дерево DOM содержит много Node (узлов). С точки зрения языка Java, Node - это интерфейс. Node является основным типом данных DOM; все в дереве DOM является Node того или иного типа. DOM Уровня 1 также определяет несколько подинтерфейсов интерфейса Node:

Дополнительными типами узлов являются: Comment, который представляет комментарий в XML-файле; ProcessingInstruction, который представляет инструкцию обработки; и CDATASection, который представляет раздел CDATA. Возможно, вам не понадобятся эти типы узлов, но если они вам понадобятся, они есть.

Примечание: Слова "элемент" и "тег" имеют разные значения. Элемент - это начальный элемент и конечный элемент, и все между ними, включая атрибуты, текст, комментарии и дочерние элементы. Тег - это пара <угловых скобок> и все между ними, включая имя элемента и любые атрибуты. Например, <p class="blue"> - это тег, также, как и </p>; тогда как <p class="blue">The quick brown fox</p> - это элемент.

Общие методы DOM

Когда вы работаете с DOM, вы часто используете следующие методы:

Ваше первое приложение-пример!

Для вашего первого приложения я предлагаю вам DomOne.java, простой код Java, который делает четыре вещи:

  1. Выбирает из командной строки имя XML-файла
  2. Создает объект парсера
  3. Приказывает объекту парсера разобрать XML-файл, названный в командной строке
  4. Обходит результирующее дерево DOM и печатает содержимое различных узлов в стандартный вывод

Вот так! Хотя вряд ли это впечатляющий код, он проиллюстрирует основные методы DOM и, как их вызывать. К более сложным вещам я перейду позже. Пока что взглянем на этот исходный код.

Шаг 1: Сканирование командной строки

Сканирование командной строки относительно легкое. Вы просто ищете аргумент и предполагаете, что аргумент является именем файла или URI. Если в командной строке нет аргументов, вы печатаете сообщение об ошибке и завершаете программу:

public static void main(String argv[])
{
  if (argv.length == 0 ||
  (argv.length == 1 && argv[0].equals("-help")))
  {
    System.out.println("\nUsage: java DomOne uri");
    System.out.println(" where uri is the URI of the XML " +
      "document you want to print.");
    System.out.println(" Sample: java DomOne sonnet.xml");
    System.out.println("\nParses an XML-document, then writes " +
      "the DOM tree to the console.");
    System.exit(1);
  }
  DomOne d1 = new DomOne();
  d1.parseAndPrint(argv[0]);
}

Все примеры в данном учебнике имеют один и тот же код для обработки командной строки; я не буду обсуждать этот код в дальнейшем.

Шаг 2: Создание объекта парсера

Предположим, что с командной строкой все в порядке, следующей задачей будет создание объекта парсера. XML-парсеры первого поколения требовали от вас создания экземпляра определенного класса. Чтобы использовать парсер Xerces из Apache XML Project, вы создавали экземпляр org.apache.xml.парсеры.DOMParser; чтобы использовать другой парсер, вы создавали экземпляр, специфический для этого парсера. Недостатком этого, конечно, является то, что переключение парсеров означало, что вам нужно изменять ваш исходный код. Для современных парсеров вы можете использовать фабричные классы, которые которые позволяют вашему коду не знать, какой класс парсера вы используете.

Вот как вы создаете фабричный объект, а затем фабричный объект создает парсер:

public void parseAndPrint(String uri)
{
  Document doc = null;
  try
  {
    DocumentBuilderFactory dbf =
    DocumentBuilderFactory.newInstance();
    DocumentBuilder db = dbf.newDocumentBuilder();
    . . .

Объект DocumentBuilder является вашим парсером. Он реализует все методы DOM, которые вам нужны для разбора и обработки XML-файла. Заметьте, что вы создаете его внутри блока try; много чего может оказаться неправильным, когда вы создаете парсер и разбираете документ, так что вам нужно вылавливать любые ошибки, которые могут произойти. Классы DocumentBuilderFactory и DocumentBuilder определены JAXP. Я коротко расскажу о JAXP, если вы не можете подождать, посмотрите Несколько слов о JAXP.

Шаг 3: Разбор XML-файла

Теперь, когда вы создали парсер, просто поручить ему разобрать файл (или URI):

doc = db.parse(uri);

Вот так! Вы должны помнить из Шага 2, что doc является экземпляром интерфейса Document. Другими словами, разбор XML-файла дает вам структуру Document, которую вы можете изучать, чтобы определить, что содержит ваш XML-файл.

Шаг 4: Распечатка содержимого дерева DOM

Последняя задача - распечатка содержимого дерева DOM. Поскольку все в дереве DOM является Node того или иного типа, вы будете использовать рекурсивный метод обхода дерева и печати всего, что есть в нем. Стратегия будет состоять в вызове метода для печати узла. Метод будет распечатывать узел, а затем вызывать себя же для каждого из потомков этого узла. Если этот потомок имеет своих потомков, метод будет вызывать себя также из него. Вот пример кода:

if (doc != null)
  printDomTree (doc);
. . .
}
public void printDomTree(Node node)
{
  int type = node.getNodeType();
  switch (type)
  {
    // print the document element
    case Node.DOCUMENT_NODE:
    {
      System.out.println("<?xml version=\"1.0\" ? >");
      printDomTree (((Document)node).getDocumentElement());
      break;
    }

Метод printDomTree принимает аргумент типа Node. Если этот Node является документным узлом, вы печатаете XML-декларацию, а затем вызываете printDomTree для элемента документа (элемента XML, который содержит остальную часть документа). Элемент документа будет, как правило, иметь потомков, так что printDomTree будет вызывать сам себя также и для каждого потомка. Этот рекурсивный алгоритм обходит все дерево DOM и печатает его содержимое.

Вот как printDomTree обрабатывает элементный узел:

case Node.ELEMENT_NODE:
{
  System.out.print("<");
  System.out.print(node.getNodeName());
  NamedNodeMap attrs = node.getAttributes();
  for (int i = 0; i < attrs.getLength(); i++)
    printDomTree (attrs.item(i));
  System.out.print(">");
  if (node.hasChildNodes())
  {
    NodeList children = node.getChildNodes();
    for (int i = 0; i < children.getLength(); i++)
      printDomTree (children.item(i));
    }
  System.out.print("</");
  System.out.print(node.getNodeName());
  System.out.print(">");
  break;
}

Для элементного узла вам нужно отпечатать левую угловую скобку и имя узла. Если этот элемент имеет атрибуты, вы вызываете printDomTree для каждого атрибута.

(Технически, вы можете использовать здесь для печати атрибутов Attr.getName() и Attr.getValue(), но я стараюсь проиллюстрировать, что все в дереве DOM является Node.)

Когда вы напечатали все атрибуты, вы вызываете printDomTree снова для каждого дочернего элемента, содержащегося в этом узле. Последним шагом является печать конечного элемента после того, как все дочерние элементы обработаны.

Обработка атрибутов и текстовых узлов тривиальна. Для атрибута вы выводите пробел, имя атрибута, знак равенства и открывающую кавычку, значение атрибута и закрывающую кавычку. (Это упрощенный способ; значение атрибута может содержать апострофы или кавычки. Вы можете исправить это, если хотите.) Обработка текстовых узлов проще всего, вы просто выводите текст. Вот код для этих двух типов узлов:

case Node.ATTRIBUTE_NODE:
{
  System.out.print(" " + node.getNodeName() + "=\"" +
  ((Attr)node).getValue() + "\"");
  break;
}
case Node.TEXT_NODE:
{
  System.out.print(node.getNodeValue());
  break;
}

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

Чтобы увидеть полный исходный код, посмотрите DomOne.java.

Выполнение DomOne

Теперь настал, которого вы, без сомнения, ожидали, - выполнения этого кода. Я предполагаю, что вы уже выгрузили и инсталлировали нужные инструменты, если нет - см. Установка вашей машины. Из каталога, куда вы разархивировали пример, введите: javac DomOne.java (Помните, что язык Java чувствителен к регистру.) Предположим, нет ошибок при вводе: java DomOne sonnet.xml для разбора и отображения файла sonnet.xml. Если все пойдет, как ожидалось, вы должны увидеть что-то вроде этого:
C:\xml-prog-java>java DomOne sonnet.xml
<?xml version="1.0" ?>
 <sonnet type="Shakespearean">
  <author>
   <lastName>Shakespeare</lastName>
   <firstName>William</firstName>
   <nationality>British</nationality>
   <yearOfBirth>1564</yearOfBirth>
   <yearOfDeath>1616</yearOfDeath>
  </author>
 <title>Sonnet 130</title>
 <lines>
  <line>My mistress' eyes are nothing like the sun,</line>
  <line>Coral is far more red than her lips red.</line>
  <line>If snow be white, why then her breasts are dun,</line>
  <line>If hairs be wires, black wires grow on her head.</line>
  . . .
 </lines>
</sonnet>

Избавление от пропусков

Результат, генерируемый DomOne, включает много пропусков, включая пропуски между элементами. Во многих случаях вы можете не обращать внимания на эти пропуски. Например, переводы строк и пробелы используемые для отступов для всех элементов <line>, не имеют значения, если все, что вы хотите, - получить текст строк сонета.

Имейте в виду, что дерево DOM заполняется объектами, так что все узлы пропусков являются на самом деле объектами Java. Эти объекты занимают память, занимают время обработки для создания и уничтожения, и ваш код должен находить их и игнорировать их. Ясно, что если вы укажете парсеру не создавать эти объекты вообще, это сбережет время и память, что всегда хорошо.

К счастью, DocumentBuilderFactory в JAXP предоставляет метод для игнорирования незначимых пропусков. Метод setIgnoringElementContentWhitespace(boolean), как следует из его длинного названия, оставляет пропуски за бортом. Я рассмотрю его далее.

DomTwo

Исходный код для DomTwo идентичен исходному коду для DomOne с одним исключением: метод setIgnoringElementContentWhitespace. Вот как выглядит метод parseAndPrint:

  public void parseAndPrint(String uri)
  {
    Document doc = null;
    try
    {
    DocumentBuilderFactory dbf =
            DocumentBuilderFactory.newInstance();
    dbf.setIgnoringElementContentWhitespace(true);
    DocumentBuilder db = dbf.newDocumentBuilder();
    doc = db.parse(uri);
    if (doc != null)
      printDomTree(doc);
. . .

Заметьте, что setIgnoringElementContentWhitespace() является методом DocumentBuilderFactory; вы устанавливаете свойство фабрики, затем запрашиваете создание парсер на основе сконфигурированных вами установок.

Если вы откомпилируете и выполните DomTwo, ваши результаты будут более короткими, но значительно менее читабельными, чем раньше:

C:\xml-prog-java>java DomTwo sonnet.xml
<?xml version="1.0" ? >
<sonnet type="Shakespearean"><author><lastName>Shakespeare
</lastName><firstName>William</firstName><nationality>Brit
ish</nationality><yearOfBirth>1564</yearOfBirth><yearOfDea
th>1616</yearOfDeath></author><title>Sonnet 130</title> <li
nes><line>My mistress' eyes are nothing like the sun,</lin
e><line>Coral is far more red than her lips red.</line><lin
e>If snow be white, why then her breasts are dun,</line><l
. . .

Хотя этот результат нелегок для чтения удаление всех этих узлов пропусков, если вам они не нужны, облегчает жизнь для вашего кода. Если вы генерируете или обрабатываете XML-документ для машины, игнорирование пропусков, возможно, является правильным путем. С другой стороны, если вам нужно форматировать информацию для человека, пропуски, возможно, являются важными.

Исходный код DomTwo см. в DomTwo.java.

Узел Document

Иногда узел Document приводит в недоумение. Узел Document отличается от корня XML-документа. Например, наш XML-сонет выглядит так:

<?xml version="1.0" ? >
<!-- Does this sonnet have a name other than "Sonnet 130"? -->
<sonnet type="Shakespearean">
 <author>
. . .
 </author>
 <title>Sonnet 130</title>
 <lines>
. . .
 </lines>
</sonnet>
<!-- The title of Sting's 1987 album "Nothing Like the Sun"
came from this sonnet. -->

В данном случае Document имеет трех потомков: узел первого комментария, элемент <sonnet>и последний комментарий. Корнем документа является элемент <sonnet>, который не является тем же самым, что и сам узел Document.

В большинстве случаев это различие не имеет значения, но имейте его в виду.

Несколько слов о JAXP

Прежде, чем я перейду к SAX API, я замечу несколько вещей по поводу JAXP, Java API for XML Parsing. Этот API определяет некоторые общие задачи, которые пропускают стандарты DOM и SAX. В частности, создание объектов парсеров не определено в стандартах DOM или SAX, а DOM не определяет включение или выключение свойств парсеров.

Пакет javax.xml.parsers является той частью JAXP, которую я обсуждаю здесь. (Другие части спецификации JAXP относятся к преобразованию XML-документов, но они лежат вне сферы данного учебника.) Пакет javax.xml.parsers определяет четыре класса:

DocumentBuilderFactory
API фабрики, который дает вам возможность создать парсер DOM. Используя API фабрики, ваш код может не знать о действительном классе, который реализует DOM. Если вы найдете лучший парсер DOM, вы можете начать использовать новый парсер без модификации или перекомпиляции вашего кода.

DocumentBuilder
Определяет API, который дает вам возможность получить объект DOM Document от парсера DOM.

SAXParserFactory
API фабрики, который дает вам возможность создать парсер SAX. Как и DocumentBuilderFactory, этот API избавляет вас от необходимости знать имя класса, который реализует парсер SAX.

SAXParser
API, который является оболочкой для класса SAX XMLReader. Когда XMLReader читает файл, он генерирует события SAX.

В этом разделе рассматривались классы DocumentBuilderFactory и DocumentBuilder; я подробнее рассмотрю SAXParserFactory и SAXParser в следующем разделе.

 

Раздел 4. SAX: Simple API for XML

Обзор SAX

В то время, как DOM была определена в W3C, в дискуссиях в Web поднимались многие вопросы. Некоторыми из этих вопросов были:

Возглавляемый в разработке David Megginson, SAX API был определен для ответа на эти вопросы. Стандарт был создан как народная разработка и виртуально поддерживается всеми XML-парсерами.

Как работает SAX

SAX - это толчковый API: вы создаете парсер SAX, и парсер сообщает вам (он проталкивает вам события), когда он находит в XML-документе разные вещи. В частности, вот как SAX решает вопросы, упомянутые выше:

События SAX

API SAX определяет много событий. Вашей работой является написание кода Java, который что-то делает, откликаясь на все эти события. Чаще всего вы используете в вашем приложении вспомогательные классы SAX. Когда вы используете вспомогательные классы, вы пишете обработчики для нескольких событий, которые вас интересуют, и предоставляете остальную работу вспомогательным классам. Если вы не обрабатываете событие, парсер удаляет его, так что вы не должны беспокоиться об использовании памяти, ненужных объектах и других проблемах, которые вы имеете, используя парсер DOM.

Имейте в виду, что события SAX не сохраняют состояние. Другими словами, вы не можете посмотреть на отдельное событие SAX и вычислить его содержимое. Если вам нужно знать, что определенная часть текста находится внутри элемента <lastName>, который находится внутри элемента <author>, на вас ложится задача отслеживать, в каком элементе находился парсер до того. Все, что событие SAX сообщает вам, это: "Вот некоторый текст". Это ваша работа - вычислить, к какому элементу этот текст относится.

Некоторые часто используемые события SAX

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

startDocument()
Сообщает вам, что парсер нашел начало документа. Это событие не передает вам никакой информации, оно просто дает вам знать, что парсер начал сканирование документа.

endDocument()
Сообщает вам, что парсер нашел конец документа.

startElement(...)
Сообщает вам, что парсер нашел начальный тег. Это событие сообщает вам имя элемента, имена и значения атрибутов элемента и дает вам также некоторую информацию пространства имен.

characters(...)
Сообщает вам, что парсер нашел некоторый текст. Вы получаете массив символов и переменные смещения в массиве и длины; вместе эти три переменные дают вам текст, который нашел парсер.

endElement(...)
Сообщает вам, что парсер нашел конечный тег. Это событие сообщает вам имя элемента, а также связанную с ним информацию пространства имен.

startElement(), characters() и endElement() - наиболее важные события; я подробно рассмотрю эти события далее. (startDocument и endDocument просто сообщают вам, что разбор начался и закончился.)

Все эти события являются частью интерфейса ContentHandler; другие интерфейсы SAX определены для обработки ошибок, а также сущностей и других редко используемых вещей.

Событие startElement()

Событие startElement() сообщает вам, что ваш парсер SAX нашел начальный тег. Событие имеет четыре параметра:

String uri
URI пространства имен. В данном примере никакие XML-документы не используют пространства имен, так что это будет пустая строка.

String localName
Неквалифицированное имя элемента.

String qualifiedName
Квалифицированное имя элемента. Это префикс пространства имен в комбинации с локальным именем элемента.

org.xml.sax.Attributes attributes
Объект, который содержит все атрибуты элемента. Этот объект предоставляет несколько методов для получения имен и значений атрибутов и количества атрибутов в элементе.

Если ваше XML-приложение ищет содержимое определенного элемента, событие startElement() сообщает вам, что элемент начался.

Примечание: Обработка пространств имен требует парсера, знающего о пространствах имен. По умолчанию парсеры, создаваемые объектами SAXParserFactory и DocumentBuilderFactory, не знают о пространствах имен. Я буду обсуждать пространства имен более подробно в новом готовящемся учебнике по продвинутому XML-программированию.

Событие characters()

Событие characters() содержит символы, которые парсер нашел в исходном файле. Следуя духу минимизации потребления памяти, это событие содержит массив символов, массив легче, чем объект Java String. Вот параметры события characters():

char[] characters
Массив символов, найденных парсером. Параметры start и length показывают ту часть массива, которую сгенерировало это событие.

int start
Индекс первого символа в массиве characters, который относится к этому событию.

int length
Число символов в этом событии.

Если вашему XML-приложению нужно сохранять содержимое определенного элемента, обработчик события characters() - это то место, куда вы помещаете код, сохраняющий содержимое.

Примечание: Стандарт SAX не определяет содержимое массива characters за пределами символов, относящихся к данному событию. Может быть возможным заглянуть в этот массив символов и найти в нем подробности предыдущих или последующих событий, но это нежелательно. Даже если данный парсер поддерживает эти лишние символы в текущем релизе, будущие релизы парсера могут изменить способ обработки символов вне текущего события.

Замечание: Может быть заманчивым заглянуть за пределы, заданные в start и length, но не надо!.

Событие endElement()

Событие endElement() сообщает вам, что парсер нашел конечный тег определенного элемента. Оно имеет три параметра:

String uri
URI пространства имен.

String localName
Неквалифицированное имя элемента.

String qualifiedName
Квалифицированное имя элемента. Это префикс пространства имен в комбинации с локальным именем элемента.

Типичной реакцией на это событие является модификация информации о состоянии в вашем XML-приложении.

Ваше первое SAX-приложение

Теперь, когда вы увидели наиболее общие события SAX, я покажу вам простое SAX-приложение. Это приложение, SaxOne.java, будет работать аналогично вашему первому DOM-приложению, DomOne.java. Это SAX-приложение будет выводить содержимое событий на консоль, так что, когда вы запустите java SaxOne sonnet.xml, результат должен быть таким же, как и полученный ранее от DomOne. Вы кодируете три задачи. Они почти идентичны четырем задачам DomOne:

Для DomOne, вы имели четыре задачи, поскольку вы обрабатывали дерево DOM после завершения разбора, поскольку здесь вы используете SAX, обработка происходит в процессе разбора. Когда разбор заканчивается, вы проходите через все события SAX.

Далее я более подробно рассмотрю эти три задачи.

SAX, шаг 1: Сканирование командной строки

Как и в DomOne, вы просто ищете в командной строке аргумент. Если аргумента нет, вы печатаете сообщение об ошибке и завершаете программу. Иначе вы предполагаете, что первый аргумент является именем или URI XML-файла:

public static void main(String argv[])
{
  if (argv.length == 0 ||
  (argv.length == 1 && argv[0].equals("-help")))
  {
    // Print an error message and exit...
  }
  SaxOne s1 = new SaxOne();
  s1.parseURI(argv[0]);
}

SAX, шаг 2: Создание объекта парсера

Ваша следующая задача - создать объект парсера. Вы используете API JAXP SAXParserFactory для создания SAXParser:

  public void parseURI(String uri)
  {
    try
    {
      SAXParserFactory spf = SAXParserFactory.newInstance();
      SAXParser sp = spf.newSAXParser ();
. . .

Это все, что вам нужно сделать. Вашим следующим шагом будет заставить SAXParser действительно разобрать файл.

(В случае, если вы пропустили краткое обсуждение JAXP, см. Несколько слов о JAXP в разделе DOM данного учебника.)

SAX, шаг 3: Разбор файла и обработка любых событий

Теперь, когда вы создали ваш объект парсера, вам нужно разобрать файл. Это делается методом parse():

    . . .
    sp.parse(uri, this);
  }
  catch (Exception e)
  {
    System.err.println(e);
  }
}

Заметьте, что метод parse() принимает два аргумента. Первым является URI XML-документа, а вторым - объект, который реализует обработчики событий SAX. В случае SaxOne вы расширяете интерфейс SAX DefaultHandler:

public class SaxOne
      extends DefaultHandler

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

Примечание: Обработка исключения, приведенная выше, является небрежной, в качестве упражнения читатель может обработать определенное исключение, такое, как SAXException или java.io.IOException.

Реализация обработчиков событий

Когда вы поручили вашему SAXParser разобрать файл, вы заказали, что SaxOne расширяет интерфейс DefaultHandler. Последнее, что я вам покажу, это, как SaxOne реализует некоторые обработчики событий в этом интерфейсе. Главное преимущество интерфейса DefaultHandler состоит в том, что он избавляет вас от необходимости реализовывать все обработчики событий. DefaultHandler реализует все обработчики событий; вы только реализуете те, которые вас интересуют.

Прежде всего, вот что делается для startDocument():

  public void startDocument()
  {
    System.out.println("<?xml version=\"1.0\"?>");
  }

Это просто мошенничество; вы просто выводите базовую XML-декларацию, независимо от того, была она в исходном XML-документе или нет. В настоящее время API SAX не возвращает подробности XML-декларации, хотя было предложено расширение API (определенное в SAX2 версии 1.1), дающее вам эту информацию.

Далее, вот что вы делаете для startElement():

  public void startElement(String namespaceURI, String localName,
        String rawName, Attributes attrs)
  {
    System.out.print("<");
    System.out.print(rawName);
    if (attrs != null)
    {
      int len = attrs.getLength();
      for (int i = 0; i < len; i++)
      {
        System.out.print(" ");
        System.out.print(attrs.getQName(i));
        System.out.print("=\"");
        System.out.print(attrs.getValue(i));
        System.out.print("\"");
      }
    }
    System.out.print(">");
  }

Вы печатаете имя элемента, затем печатаете все его атрибуты. Когда вы это сделали, вы печатаете закрывающую угловую скобку (>). Как я отмечал выше, для пустых элементов (таких элементов, как элемент XHTML <hr/>), вы получаете событие startElement(), за которым следует событие endElement(). У вас нет способа узнать, было в исходном документе закодировано <hr/> или <hr></hr>.

Еще об обработке событий

Следующим важным обработчиком является обработчик characters():

  public void characters(char ch[], int start, int length)
  {
    System.out.print(new String(ch, start, length));
  }

Поскольку все, что вы делаете, это печать XML-документа на консоль, вы просто печатаете часть массива символов, которая относится к этому событию.

Следующие два обработчика, которые я вам покажу, это endElement() и endDocument():

  public void endElement(String namespaceURI, String localName,
          String rawName)
  {
    System.out.print("</");
    System.out.print(rawName);
    System.out.print(">");
  }
  public void endDocument()
  {
  }

Когда вы получаете событие endElement(), вы просто выводите конечный тег. Вы ничего не делаете в endDocument(), но он перечислен здесь для полноты. Поскольку интерфейс DefaultHandler по умолчанию игнорирует endDocument(), вы можете удалить этот метод из вашего кода и получить тот же результат.

Далее я рассмотрю обработку ошибок в SAX-приложениях.

Обработка ошибок

SAX определяет интерфейс ErrorHandler; это один из интерфейсов, реализуемых DefaultHandler. ErrorHandler содержит три метода: warning, error и fatalError.

warning относится к предупреждению, условию, не определенному, как ошибка или фатальная ошибка спецификацией XML. error, как явствует из названия, относится к событию ошибки; например, спецификация XML определяет нарушение правил проверки правильности как ошибку. Я не рассматриваю проверку правильности, но сонет, который не содержит 14 элементов <line>, должен быть ошибочным. Наконец, fatalError также определен спецификацией XML; значение атрибута, не заключенное в кавычки, должно быть фатальной ошибкой.

Тремя событиями, определенными в ErrorHandler являются:

warning()
Проблема, относящаяся к предупреждениям, как определено в спецификации XML.

error()
Проблема, относящаяся к тому, спецификация XML рассматривает, как ошибку.

fatalError()
Проблема, относящаяся к фатальным ошибкам, как определено (как вы догадались) в спецификации XML.

Предупреждения и ошибки обычно относятся к таким вещам, как сущности и DTD (темы, которые я не обсуждаю здесь), тогда как фатальные ошибки обычно относятся к нарушениям основного синтаксиса XML (например, теги не вложены корректно). Вот синтаксис трех обработчиков ошибок:

  public void warning(SAXParseException ex)
  {
    System.err.println("[Warning] "+
    getLocationString(ex)+": "+
    ex.getMessage());
  }
  public void error(SAXParseException ex)
  {
    System.err.println("[Error] "+
    getLocationString(ex)+": "+
    ex.getMessage());
  }
  public void fatalError(SAXParseException ex)
             throws SAXException
  {
    System.err.println("[Fatal Error] "+
    getLocationString(ex)+": "+
    ex.getMessage());
    throw ex;
  }

Приведенные выше обработчики используют закрытый метод с именем getLocationString, чтобы получить более подробную информацию об ошибке. Класс SAXParseException определяет такие методы, как getLineNumber() и getColumnNumber() для предоставления строки и столбца, где произошла ошибка. getLocationString только форматирует эту информацию в строку; помещение этого кода в отдельный метод означает, что вы не должны включать его в каждый обработчик ошибки. Все подробности см. в SaxOne.java.

Выполнение SaxOne

Выполнение SaxOne предельно просто. Прежде всего, убедитесь, что вы установили XML-парсер и кодовые примеры, как описано в Установка вашей машины. Далее просто введите java SaxOne sonnet.xml. Вы должны увидеть что-то такое:

C:\xml-prog-java>java SaxOne sonnet.xml
<?xml version="1.0" ?>
<sonnet type="Shakespearean">
 <author>
  <lastName>Shakespeare</lastName>
  <firstName>William</firstName>
  <nationality>British</nationality>
  <yearOfBirth>1564</yearOfBirth>
  <yearOfDeath>1616</yearOfDeath>
 </author>
 <title>Sonnet 130</title>
 <lines>
  <line>My mistress' eyes are nothing like the sun,</line>
  <line>Coral is far more red than her lips red.</line>
  <line>If snow be white, why then her breasts are dun,</line>
  <line>If hairs be wires, black wires grow on her head.</line>
  . . .
 </lines>
</sonnet>

Чтобы увидеть полный код, посмотрите SaxOne.java.

Действия с пропусками в SAX-приложениях

Вы, возможно, заметили, что выполнение SaxOne дало вам красиво форматированный XML. Это потому, что вы сохранили все узлы пропусков, которые были в исходном XML-документе. SaxOne реализует метод ignorableWhitespace():

  public void ignorableWhitespace(char ch[], int start, int length)
  {
    characters(ch, start, length);
  }
Все, что делает этот метод - вызов события characters() с теми же параметрами (ignorableWhitespace() и characters() имеют одинаковую сигнатуру). Если вы хотите убрать пропуски, просто удалите обработчик ignorableWhitespace() из вашего кода. Вот результат приложения SaxTwo:
C:\xml-prog-java>java SaxTwo sonnet.xml
<?xml version="1.0" ?>
<sonnet type="Shakespearean"><author><lastName>Shakespeare
</lastName><firstName>William</firstName><nationality>Brit
ish</nationality><yearOfBirth>1564</yearOfBirth><yearOfDea
th>1616</yearOfDeath></author><title>Sonnet 130</title><li
nes><line>My mistress' eyes are nothing like the sun,</lin
e><line>Coral is far more red than her lips red.</line><lin
e>If snow be white, why then her breasts are dun,</line><l
. . .

Если вы хотите увидеть исходный код SaxTwo, посмотрите SaxTwo.java.

 

Раздел 5. JDOM

Обзор JDOM

Другим ответом на осознание сложности Объектной Модели Документа является JDOM. Созданная Brett McLaughlin и Jason Hunter, она использует правило 80-20 для предоставления простого API для 80% наиболее общих функций обработки XML. Она не пытается заменить DOM и работает в настоящее время только с языком Java.

Как вы увидите в этом разделе, JDOM делает некоторые общие задачи предельно простыми.

Разбор прост. Добавление составляющих к разобранному XML-документу просто. Запись разобранного XML-документа как XML-файла проста.

Как отмечается в правиле 80-20 rule: В промышленности программного обеспечения, 80% самого полезного в вашей программе содержится в 20% вашего кода. Целью JDOM является написание этих 20% и предоставление простого API для них. Первая формулировка правила 80-20 была сделана итальянским экономистом Vilfredo Pareto в 1906, когда он обнаружил, что 80% земли в Италии принадлежит 20% людей. Принцип Парето (или правило 80-20) был концептуализирован в 1930-е специалистом по качеству д-ром Joseph Juran; в последующие годы ученые во многих других дисциплинах заметили, что 20% чего-то часто ответственны за 80% результата. Если вы хотите произвести впечатление на ваших друзей подробностями правила 80-20, посетите http://www.juran.com/search_db.cfm и поищите "Pareto."

Цели JDOM

Цитата с Web-сайта JDOM (см. Ресурсы), "Нет непреодолимых причин, по которым API Java для работы с XML должен быть сложным, запутанным, неинтуитивным или занудным." При использовании JDOM XML-документы работают как объекты Java и используют коллекции Java. Я покажу вам два JDOM-приложения, похожие на DomOne и DomTwo; вы заметите, что эти приложения гораздо меньшие.

Важно отметить, что JDOM интегрируется и с DOM, и с SAX. Вы можете использовать деревья DOM или события SAX для построения документов JDOM. Если у вас есть документ JDOM, вы можете легко конвертировать его в дерево DOM или в события SAX.

JdomOne

Как вы и ожидаете, код для JdomOne структурно аналогичен DomOne и SaxOne. К счастью, код, который вы должны написать, гораздо меньше. Сначала вы ищете в командной строке имя XML-файла, затем у вас идет логика программы:

  try
  {
    SAXBuilder sb = new SAXBuilder();
    Document doc = sb.build(new File(argv[0]));
    XMLOutputter xo = new XMLOutputter();
    xo.output(doc, System.out);
  }
  catch (Exception e)
  {
    e.printStackTrace();
  }

Вот так! Заметьте, что JDOM определяет класс XMLOutputter, который выводит XML-файл в командную строку. Вывод разобранного документа как XML является общей задачей, так что JDOM предоставляет класс для выполнения этого для вас. Для первых двух строк кода и SAXBuilder, и Document являются частью пакета JDOM (org.jdom.input.SAXBuilder и org.jdom.Document соответственно). Если вы комбинируете JDOM и DOM, убедитесь, что некоторые классы (такие, как Document) определены в обоих пакетах. Вы должны полностью квалифицировать имя этого класса, чтобы компилятор Java знал, о каком Document вы говорите.

Выполнение JdomOne

Прежде, чем вы запустите JDOM-приложение, вам нужно установить вашу машину, как описано в Установка вашей машины. Выполнение JdomOne дает вам тот же результат, что и ваши прежние приложения DOM и SAX:

C:\xml-prog-java>java JdomOne sonnet.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sonnet SYSTEM "sonnet.dtd">
<sonnet type="Shakespearean">
 <author>
  <lastName>Shakespeare</lastName>
  <firstName>William</firstName>
  <nationality>British</nationality>
  <yearOfBirth>1564</yearOfBirth>
  <yearOfDeath>1616</yearOfDeath>
 </author>
 <title>Sonnet 130</title>
 <lines>
  <line>My mistress' eyes are nothing like the sun,</line>
  <line>Coral is far more red than her lips red.</line>
  <line>If snow be white, why then her breasts are dun,</line>
  <line>If hairs be wires, black wires grow on her head.</line>
  . . .
 </lines>
</sonnet>

Заметьте, что JDOM добавил атрибут encoding в XML-декларацию и добавил декларацию DOCTYPE. Полный исходный код находится в JdomOne.java.

Удаление пропусков

Чтобы обеспечить JDOM-версию DomTwo, вам нужно удалить все игнорируемые пропуски. Как вы и ожидаете, JDOM позволяет сделать это легко. Так выглядит код:

  SAXBuilder sb = new SAXBuilder();
  Document doc = sb.build(new File(argv[0]));
  XMLOutputter xo = new XMLOutputter();
  xo.setTrimAllWhite(true); 
  xo.output(doc, System.out);

Все, что вы должны сделать, это добавить в ваш код метод setTrimAllWhite(). Вы приказываете XMLOutputter удалить все пробелы и получаете такой результат, как ожидали.

Полный пример находится в JdomTwo.java.

Прелести JDOM

Теперь вы имеете представление о простоте JDOM. Вы должны внимательнее рассмотреть этот API; он делает все, что вам нужно, он может значительно упростить ваш процесс разработки. Лучше всего, что JDOM имеет адаптеры для всех общих XML-парсеров (Xerces от Apache XML Project, Oracle's XML parser, Crimson от Sun, адаптер для API JAXP и адаптер для парсера IBM's XML4J).

Хотя это и вне сферы внимания нашего учебника, вы можете также создать ваш собственный адаптер, чтобы сделать другие источники данных выглядящими, как XML-документы. Например, вы можете заставить информацию из реляционной базы данных или из каталога LDAP выглядеть как обычное дерево DOM. Вы можете затем использовать методы JDOM для манипулирования данными любым способом, каким захотите.

 

Раздел 6. Резюме

Резюме

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

Я надеюсь, это поможет вам начать работу с собственными XML-приложениями на языке Java.

Что не было рассмотрено

Чтобы не увеличивать объем данного учебника, я не рассматривал многие из продвинутых тем. Я рассмотрю их все в продвинутом учебнике по XML-программированию здесь, на developerWorks. Некоторые из тем, которые будут рассмотрены, такие:

 

Ресурсы

Обучение

 

Об авторе

Doug Tidwell

(Эта биография написана Lily Castle Tidwell, дочерью автора.)

Моего папу зовут Doug Tidwell. У него черные волосы. Папа носит очки. Он шести футов росту. Его глаза карие.

Папа любит играть со мной в пятнашки. Он любит бегать. Он по-настоящему быстрый. Он по-настоящему хороший мотоциклист. Его мотоцикл синий.

Работа моего папы - путешествия. Он часто ездит в Китай. Он ходил по Великой Стене в Китае, о чем получил сертификат.

Я ЛЮБЛЮ ТЕБЯ, ПАПА!

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

Вы можете связаться с автором, Doug Tidwell, по адресу dtidwell@us.ibm.com.


КаталогИндекс раздела