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


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

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

Doug Tidwell

16 июля 2004

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

Содержание

  1. Введение
  2. Построение структур XML с нуля
  3. Преобразование из одного API в другой
  4. Манипулирование структурами дерева
  5. Продвинутые свойства DOM
  6. Продвинутые свойства SAX
  7. Резюме и ссылки
  8. Об авторе

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

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

В первом учебнике я показал вам основы разбора XML в языке Java. Я рассмотрел главные API (DOM, SAX и JDOM) и провел вас через несколько примеров, которые демонстрировали основные задачи, общие для большинства XML-приложений. Во втором учебнике серии я рассмотрел свойства парсера, пространства имен и проверку правильности XML. Этот последний учебник рассмотрит более трудные вещи, которые не рассматривались раньше, такие как:

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

Как и предыдущих учебниках, я рассматриваю такие API:

Хотя многие из программных примеров, которые я здесь обсуждаю, используют JAXP (Java API for XML parsing), я не буду специально обсуждать JAXP в данном учебнике.

О примерах

Большинство примеров здесь будут работать с сонетом Шекспира, который появился в предыдущих учебниках. Структура сонета такая:

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

Я использую этот документ-пример через весь данный учебник. Полный набор файлов-примеров перечислен ниже:

Как альтернатива, вы можете выгрузить x-java3_codefiles.zip и посмотреть эти файлы в текстовом редакторе.

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

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

Вам следует установить несколько вещей на вашей машине прежде, чем вы сможете выполнять примеры. (Предполагается, что вы знаете, как компилировать и запускать 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. Наконец, выгрузите архив с примерами для этого учебника, x-java3_codefiles.zip, и распакуйте файл.
  6. Добавьте текущий каталог (.), xercesImpl.jar, xml-apis.jar и jdom.jar в вашу переменную CLASSPATH.

 

Раздел 2. Построение структур XML с нуля

Разбор строки

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

Трюк состоит в конвертировании String языка Java в org.xml.sax.InputSource. Парсер - все равно DOM или SAX (используется DocumentBuilder или SAXParser JAXP) - может взять InputSource и разобрать его, точно так же, как и любую другую разметку. Чтобы конвертировать String, код такой:

ParseString ps = new ParseString();
String markup = new String("<html><body><h1>" +
    "This XML document was a " +
    "<b>string!</b>" +
    "</h1></body></html>");
InputSource iSrc = new InputSource(new StringReader(markup)); 
ps.parseAndPrint(iSrc);

Здесь вы создаете InputSource из StringReader, который вы создаете из String. Внутри метода parseAndPrint код очень похож на примеры разбора в предыдущих учебниках:

public void parseAndPrint(InputSource xmlSource)
{
  Document doc = null;
  try
  {
    DocumentBuilderFactory dbf =
      DocumentBuilderFactory.newInstance();
    DocumentBuilder db = dbf.newDocumentBuilder();
    doc = db.parse(xmlSource);
    if (doc != null)
      DomTreePrinter.printNode(doc);
  }

Единственное изменение состоит в том, что метод принимает на входе InputSource вместо URL. Чтобы напечатать строку, вы используете класс com.ibm.dw.xmlprogjava.DomTreePrinter. Полный исходный код находится в файлах ParseString.java и DomTreePrinter.java.

Построение с нуля дерева DOM

В ранних DOM-приложениях вы получали дерево DOM от парсера после того, как он разобрал XML-файл. Иногда вы можете захотеть создать дерево DOM без исходного файла XML. Например, вам может понадобиться конвертировать результаты запроса SQL в дерево DOM, а затем использовать инструменты XML по дереву DOM.

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

Как и следовало ожидать, вам нужно начать с запроса к фабрике объектов создать DocumentBuilder:

try
{
  DocumentBuilderFactory dbf =
    DocumentBuilderFactory.newInstance();
  DocumentBuilder docBuilder = dbf.newDocumentBuilder();
  Document doc = docBuilder.getDOMImplementation().
  createDocument("", "sonnet", null);
. . .

Начните с создания нового DocumentBuilderFactory и нового DocumentBuilder, как и прежде. Затем вызовите DocumentBuilder.getDOMImplementation(), чтобы получить экземпляр чего-то, что реализует интерфейс DOMImplementation. Используйте метод этого объекта createDocument для получения нового объекта Document. (Замечание: DOMImplementation является частью DOM, а не JAXP.) В данном примере три аргумента метода createDocument задают, что ваш новый Document не имеет пространства имен, имя корневого элемента - sonnet, и Document не имеет DOCTYPE.

Использование DocumentElement

Теперь, когда вы создали Document, вам нужно добавлять в него. Кодовый пример ниже начинается с получения DocumentElement из объекта Document. Отличия между ними едва различимы, но существенны: объект Document является полной структурой, которая представляет разобранную версию XML-документа; DocumentElement является корневым элементом, который содержит всю разметку XML. (Комментарии могут появляться за пределами корневого элемента XML-документа; эти комментарии будут в объекте Document, но не в DocumentElement.) В XML-файле примера элемент <sonnet> содержит остальную часть документа.

. . .
Document doc = docBuilder.getDOMImplementation().
  createDocument("", "sonnet", null);
Element root = doc.getDocumentElement();
root.setAttribute("type", "Shakespearean");
Element author = doc.createElement("author");
Element lastName = doc.createElement("lastName");
lastName.appendChild(doc.createTextNode("Shakespeare"));
author.appendChild(lastName);
. . .
Element yearOfDeath = doc.createElement("yearOfDeath");
yearOfDeath.appendChild(doc.createTextNode("1616"));
author.appendChild(yearOfDeath);
root.appendChild(author);
. . .

В этом листинге вы устанавливаете атрибут type корневого элемента (<sonnet>), затем вы создаете элемент <author>. Через весь код объект Document используется как фабрика для создания новых Node. Ваш код должен также создать иерархию документа. Чтобы построить элемент <author>, вы создаете сам элемент <author>, а затем создаете другие элементы, содержащиеся в <author> (<lastName>, <firstName> и т.д.). По мере создания дочерних элементов для <author>, вы добавляете их к элементу <author>. Когда элемент <author> завершен, вы добавляете его к родительскому элементу, <sonnet>. Наконец, заметьте некоторую неуклюжесть в добавлении текста к элементу. Чтобы создать такую разметку:

<yearOfDeath>1616</yearOfDeath>

вы должны создать элемент <yearOfDeath>, затем создать текстовый узел, содержащий текст 1616, затем добавить текстовый узел к элементу <yearOfDeath>, и затем вы можете добавить элемент <yearOfDeath> к элементу <author>. Для того, чтобы добавить текстовый узел к элементу, нет метода вроде Element.setText(), как можно было бы ожидать. Особенности вроде этой были тем, что привело к созданию JDOM; я покажу вам, как строить XML-документ при помощи JDOM через минуту.

Выполнение DomBuilder

Чтобы выполнить приложение, просто введите java DomBuilder в командной строке. Вы увидите такой вывод:

C:\adv-xml-prog>java DomBuilder
<?xml version="1.0" ?>
<sonnet type="Shakespearean"><author><lastName>Shakespeare
</last-name><firstName>William</firstName><nationality>Bri
tish</nationality><yearOfBirth>1564</yearOfBirth><yearOfDe
ath>1616</year-of-death></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,</lin
e><line>If hairs be wires, black wires grow on her head.<l

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

Позже в разделе Генерация событий SAX из разделенных запятыми значений я покажу, как генерировать события SAX из различных источников. Пока что вы можете посмотреть полный исходный листинг DomBuilder в DomBuilder.java.

Построение с нуля JDOM Document

Построение Document в JDOM является, как и следовало ожидать, значительно более легким, чем DOM-версия. Как вы можете вспомнить из целей JDOM, создание Document работает так же, как создание объекта Java. Вот как этот код начинается:

public void buildDocument()
{
  Element root = new Element("sonnet");
  root.setAttribute("type", "Shakespearean");
  Vector author = new Vector();
  author.add(new Element("lastName").addContent("Shakespeare"));
  author.add(new Element("firstName").addContent("William"));
  author.add(new Element("nationality").addContent("British"));
  author.add(new Element("yearOfBirth").addContent("1564"));
  author.add(new Element("yearOfDeath").addContent("1616"));
  root.addContent(new Element("author").setContent(author));
  root.addContent(new Element("title").addContent("Sonnet 130"));

Заметьте, насколько этот код проще. Например, при добавлении текста к узлу в DOM, вы должны создать текстовый узел (используя узел документа как фабрику элементов), затем сделать текстовый узел потомком элемента, затем сделать этот элемент потомком другого элемента и т.д. В JDOM вы можете создать Vector элементов, а затем использовать метод setContent для добавления в Vector какого-то другого элемента, как потомка.

После того, как вы установили содержимое вашего элемента root, вы можете создавать объект Document JDOM в нем:

  Vector lines = new Vector();
  lines.add(new Element("line").
  addContent("My mistress' eyes are nothing like the sun,"));
. . .
  lines.add(new Element("line").
  addContent("As any she belied with false compare."));
  root.addContent(new Element("lines").setContent(lines));
  Document doc = new Document(root,
  new DocType("sonnet", "sonnet.dtd"));
  try
  {
    XMLOutputter xo = new XMLOutputter(" ", true);
    xo.output(doc, System.out);
  }

Как и в JDOM-приложениях из предыдущих учебников, вы используете XMLOutputter для вывода документа на консоль. Заметьте, что JDOM дает вам возможность создать декларацию DOCTYPE при создании объекта Document.

Полный исходный код см. в JdomBuilder.java.

Генерация событий SAX из разделенных запятыми значений

Последний пример генерации в SAX иллюстрирует, как сгенерировать события SAX из не-XML источника данных. Эти приемы исключительно полезны. Любой код, который обрабатывает данные, может генерировать события SAX из этих данных, давая возможность SAX-парсеру трактовать данные как источник данных XML. В следующем разделе, Преобразование из одного API в другой, содержится пример, названный SaxToDom, который преобразует события SAX в объекты DOM; комбинирование этих двух приемов дает вам исключительно гибкий способ обрабатывать много различных видов данных.

В данном примере используемый источник данных является файлом со значениями, разделенными запятыми, называемый также CSV-файлом. (Хорошим примером будет также код, который обращается к базе данных через драйвер JDBC, но соединение с базой данных более сложно.) Для разбора файла вы будете использовать объект Java StreamTokenizer, а затем будете генерировать соответствующий XML из найденных в данных лексем.

Вот несколько строк из файла-примера, test.csv:

"000010","CHRISTINE","I","HAAS","A00","3978",19650101
"000020","MICHAEL","L","THOMPSON","B01","3476",19731010
"000030","SALLY","A","KWAN","C01","4738",19750405
"000050","JOHN","B","GEYER","E01","6789",19490817

Этот листинг был сгенерирован из запроса SQL, в каждой строке содержится семь полей данных. Используйте следующие имена элементов XML для окружения этих данных:

static String tagNames[] = {"employeeNumber",
                  "firstName",
                  "middleInitial",
                  "lastName",
                  "deptNo",
                  "extension",
                  "dateOfBirth"};

CSV в SAX, продолжение

Для начала установите StreamTokenizer:

BufferedReader br = new BufferedReader(new FileReader(uri));
StreamTokenizer st = new StreamTokenizer(br);
st.eolIsSignificant(true);
st.whitespaceChars(',', ',');
st.quoteChar('"');

Этим определяются свойства StreamTokenizer. Теперь определите три массива символов, которые вы будете использовать для красивой печати XML на выходе:

char [] lineBreak = new String("\n").toCharArray();
char [] singleIndent = new String(" ").toCharArray();
char [] doubleIndent = new String(" ").toCharArray();

По мере разбора составляющих в CSV-файле мы проходим через следующие шаги:

  1. Генерация события startDocument.
  2. Генерация события startElement для элемента <employees>.
  3. Для каждой строки выполнить следующее:
    1. Генерация события startElement для элемента <employee>.
    2. Перебор всех лексем в этой строке. Вы будете использовать статический ваш массив имен тегов для генерации элементов XML; например, первый элемент будет <employeeNumber>, второй будет <firstName> и т.д. Для каждой лексемы генерируйте startElement для элемента, генерируйте characters для текста, затем генерируйте endElement.
    3. Генерация события endElement для элемента <employee>.
  4. Генерация события endElement для элемента <employees>.
  5. Генерация события endDocument.

Я покажу вам этот код в деталях далее.

Генерация событий SAX

Прежде всего, генерируйте событие startDocument. Вы знаете, что вы должны вложить весь XML-документ в элемент <employees>, так что вы можете двигаться дальше и сгенерировать startElement. После обработки всего документа вы сгенерируете событие endElement для элемента <employees>. Вот код:

dh.startDocument();
dh.startElement(null, null, "employees", null);
dh.ignorableWhitespace(lineBreak, 0, lineBreak.length);

(Заметьте, что для добавления перевода строки в вывод генерируется событие ignorableWhitespace.)

Тут вы должны установить вложенные циклы while для обработки строк файла. Внешний цикл while выполняется до тех пор, пока StreamTokenizer не вернет тип, соответствующий маркеру конца файла (StreamTokenizer.TT_EOF). В каждой итерации цикла вызывайте startElement для элемента <employee>, обрабатывайте все составляющие в текущей строке исходного файла, затем вызывайте endElement для <employee>. Внутренний цикл обрабатывает каждую строку до тех пор, пока StreamTokenizer найдет либо маркер конца строки (TT_EOL), либо маркер конца файла (TT_EOF). Вот первая часть кода:

  st.nextToken();
  while (st.ttype != StreamTokenizer.TT_EOF)
  {
    dh.ignorableWhitespace(singleIndent, 0, singleIndent.length);
    dh.startElement(null, null, "employee", null);
    dh.ignorableWhitespace(lineBreak, 0, lineBreak.length);
    int i = 0;
    while (st.ttype != StreamTokenizer.TT_EOL &&
    st.ttype != StreamTokenizer.TT_EOF)
    {

Заметьте, что вы используете переменную i для подсчета того, сколько элементов нашел сканер (tokenizer). Используйте его для выборки имени элемента из массива, обсуждавшегося ранее.

Класс StreamTokenizer

Прежде, чем я закончу, - несколько слов о работе класса StreamTokenizer. В первый раз, когда вы используете этот класс, вам нужно вызывать метод nextToken. Этот метод приказывает сканеру найти следующую лексему в файле (в данном случае - первую). Теперь вы можете получить лексему от объекта-сканера использованием поля nval для числовых значений и поля sval для строковых значений. Вы используете также поле ttype для определения типа лексемы. Код для преобразования числовой лексемы в серию событий SAX такой:

  if (st.ttype == StreamTokenizer.TT_NUMBER)
  {
    char [] chars = BigInteger.valueOf((long)st.nval).
    toString().toCharArray();
    dh.ignorableWhitespace(doubleIndent, 0, doubleIndent.length);
    dh.startElement(null, null, tagNames[i], null);
    dh.characters(chars, 0, chars.length);
    dh.endElement(null, null, tagNames[i]);
    dh.ignorableWhitespace(lineBreak, 0, lineBreak.length);
  }

Класс Java BigInteger используется здесь для обработки длинных больших значений, которые могут появляться в файлах с разделителем-запятой. (Например, даты в реляционных базах данных часто кодируются как 8-значные числа.) Преобразовывайте числовые значение в массив символов, а затем вызывайте соответствующие события SAX.

В конце вложенного цикла, наращивайте счетчик (i) и вызывайте метод StreamTokenizer.nextToken, чтобы продвинуть сканер. Когда внутренний цикл заканчивается, вызывайте endElement для элемента <employee>. Когда внешний цикл заканчивается, вызывайте endElement для элемента <employees>, за которым следует событие:

      st.nextToken();
      i++;
    }
    dh.ignorableWhitespace(singleIndent, 0, singleIndent.length);
    dh.endElement(null, null, "employee");
    st.nextToken();
    dh.ignorableWhitespace(lineBreak, 0, lineBreak.length);
    }
  dh.endElement(null, null, "employees");
  dh.ignorableWhitespace(lineBreak, 0, lineBreak.length);
  dh.endDocument();
  }

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

Полный исходный код вы можете увидеть в CsvToSax.java. Файл с данными примера - test.csv.

 

Раздел 3. Преобразование из одного API в другой

Преобразование событий SAX в деревья DOM

Обычно XML-приложение использует SAX, однако, вы можете захотеть использовать оба интерфейса вместе. Предположим, например, вы имеете систему отчетов, которая генерирует счета для 10,000 пользователей. Эти счета создаются как один XML-файл, содержащий элементов 10,000 <invoice>. Для обработки каждого <invoice> вы хотите использовать дерево DOM. К сожалению, у вас нет машины с достаточным объемом памяти для создания дерева DOM, содержащего потенциально миллион объектов, представляющих эти 10,000 счетов.

Для данного примера вы можете использовать гибридный подход. Для разбора XML-файла примените SAX-парсер, используйте все события SAX для данного <invoice>, чтобы построить дерево DOM. Когда вы получаете событие startElement для элемента <invoice>, вы создаете новое дерево DOM. Как только ваш код получает события SAX, вы добавляете новые Node в дерево DOM. Когда вы получаете событие endElement для <invoice>, вы передаете дерево DOM в вашу процедуру обработки счета. После обработки текущего <invoice> вы you можете удалить дерево DOM и начать сначала с новым рядом событий SAX.

Отображение событий SAX на объекты DOM

Для преобразования событий SAX в дерево DOM, рассмотрим наиболее общие события SAX:

startDocument
Естественно подумать, что вы будете использовать это событие, чтобы поручить вашему объекту DocumentBuilder создать Document. К сожалению, startDocument не сообщает вам имя корневого элемент, так что вы должны делать это в обработчике startElement.

startElement
Вы должны обрабатывать два случая startElement:

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

ignorableWhitespace
Если вы хотите включать пропуски в дерево DOM, создайте новый узел Text, содержащий пропуск. Как и с обычными событиями characters, вы можете добавить его к узлу, находящемуся в вершине стека.

endElement
endElement означает, что парсер нашел конец элемента. Это значит, что вам нужно выбрать из стека законченный элемент, а затем добавить его в качестве потомка к элементу, который теперь находится в вершине стека. (Чтобы избежать EmptyStackException, убедитесь, что в стеке есть родительский элемент.)

endDocument
Игнорируйте это событие. После того, как вы обработали последнее событие endElement, стек будет содержать единственную составляющую, корневой элемент документа.

Далее я покажу вам обработчики событий в SaxToDom и продемонстрирую, как вы можете создавать объекты DOM по мере поступления событий SAX.

Использование стека

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

По мере получения событий от SAX-парсера вы можете преобразовывать каждое событие в соответствующий тип Node DOM. Если вы создали Node, вам нужно знать его родителя. Наиболее эффективным способом сделать это является стек, в частности, java.util.Stack. Обрабатывайте различные типы Node DOM следующим образом:

Вы в вашем коде игнорируете другие типы узлов, такие как ProcessingInstruction и Comment. Если вы будете создавать узлы этих типов, вы просто будете добавлять их к элементу в вершине стека. (Если вы реализуете это, имейте в виду, что комментарии и инструкции обработки могут появляться вне корневого элемента.)

Обработчик события startElement

Для обработки события startElement вы обычно создаете новый Element DOM и помещаете его в стек. Однако, эта ситуация имеет дополнительное осложнение: для первого события startElement вам нужно создать новый объект DOM Document. Все другие объекты DOM, которые вы будете создавать, будут потомками объекта Document. Вот код:

if (firstElementNotFoundYet)
{
  root = docBuilder.getDOMImplementation().
  createDocument(namespaceURI, rawName, null);
  Element docElement = root.getDocumentElement();
  if (attrs != null)
  {
    int len = attrs.getLength();
    for (int i = 0; i < len; i++)
      docElement.setAttribute(attrs.getQName(i),
        attrs.getValue(i));
  }
  elementStack = new Stack();
  elementStack.push(docElement);
  firstElementNotFoundYet = false;
}
else
{
  Element currentElement = root.createElement(rawName);
  if (attrs != null)
  {
    int len = attrs.getLength();
    for (int i = 0; i < len; i++)
      currentElement.setAttribute(attrs.getQName(i),
        attrs.getValue(i));
    }
  elementStack.push(currentElement);
}

Здесь вызовы методов - таких, как createDocument(), getDocumentElement() и createElement() - такие же, как вы применяли в DomBuilder. Главное отличие в обработке здесь состоит в использовании elementStack для отслеживания последнего созданного вами Element.

Обработчики событий characters и ignorableWhitespace

Вы можете обрабатывать эти события созданием нового текстового узла и добавлением его к Element, который находится в вершине стека. Вы можете использовать метод Stack.peek() для обращения к верхнему элементу стека без удаления его из стека. Коды этих двух обработчиков событий такие:

public void ignorableWhitespace(char ch[], int start, int length)
{
  characters(ch, start, length);
}
public void characters(char ch[], int start, int length)
{
  ((Element) elementStack.peek()).
    appendChild(root.createTextNode(new String(ch, start, length)));
}

Обработчик события ignorableWhitespace просто вызывает обработчик события characters. В обработчике события characters вы можете использовать метод peek() для обращения к элементу в вершине стека. Заметьте, что вы должны преобразовать элемент стека в Element; метод peek() возвращает Object языка Java. Вы можете затем создать новый текстовый узел и добавить его к Element в вершине стека.

Обработчик события endElement

Для обработки события endElement вам нужно удалить составляющую из вершины стека и добавить ее в ту составляющую, которая прежде была под ней. Единственным исключением является событие endElement в конце документа; для этого события стек имеет только одну составляющую. Вот как выглядит код:

public void endElement(String namespaceURI, String localName,
String rawName)
{
  if (elementStack.size() > 1)
  {
    Element currentElement = (Element) elementStack.pop();
    ((Element) elementStack.peek()).appendChild(currentElement);
  }
}

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

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

Завершение всего этого

Все, что осталось сделать теперь, это создать объект-парсер, который будет разбирать файл и строить дерево DOM из событий SAX. Исходный код метода parseAndPrint такой:

Document root = null; // global variable
. . .
public void parseAndPrint(String uri)
{
  try
  {
    dbf = DocumentBuilderFactory.newInstance();
    docBuilder = dbf.newDocumentBuilder();
    SAXParserFactory spf = SAXParserFactory.newInstance();
    SAXParser sp = spf.newSAXParser();
    sp.parse(uri, this);
    if (root != null)
      printDomTree(root);
   }
. . .

Вы создаете DocumentBuilder, который применяется для создания объектов DOM. Вы также создаете объект SAXParser для разбора XML-файла и генерации событий SAX.

Когда вы запустите пример, вы увидите:

C:\adv-xml-prog>java SaxToDom sonnet.xml

<?xml version="1.0"?>
<!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>
. . .

Полный листинг исходного кода см. в SaxToDom.java.

Генерация событийSAX из дерева DOM

Теперь вы увидели, как создать дерево DOM из событий SAX. Далее я покажу вам, как генерировать события SAX из дерева DOM. Я покажу вам это в упрощенном виде; я не уверен, что кому-то понадобится делать это. (Если у вас есть соображения по использованию этого приема, пожалуйста, дайте мне знать об этом.)

Сначала взглянем на отображение между узлами дерево DOM и событиями SAX:

DOCUMENT_NODE
Вызвать startDocument(), затем обработать все в узле документа, затем вызвать endDocument(). Чтобы обработать все в документе, используйте рекурсивную технику, которая использовалась в примерах DOM.

ELEMENT_NODE
Теперь рассмотрите элемент, чтобы выбрать все атрибуты, которые у него есть; вам нужно сделать это, когда вы вызываете startElement(). После того, как вы вызвали startElement(), вы будете обрабатывать всех потомков элемент, а затем вызывать endElement().

TEXT_NODE
Для этого типа узла вы просто создаете массив char, который содержит значение узла.

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

Далее я покажу вам этот код.

Создание событий SAX

Я определю, как мы собираемся отображать узлы DOM на различные типы событий SAX, так что, теперь пора взглянуть на код. Шаги, через которые вы должны пройти, следующие:

  1. Создать DOM-парсер.
  2. Разобрать файл и создать дерево DOM.
  3. Обойти дерево DOM, преобразуя узлы DOM в соответствующие события SAX.
  4. По мере генерации событий SAX применять процедуру печати из SAX, которую вы разработали раньше (в первом учебнике) для печати событий SAX.

Подробности см. в исходном коде DomToSax.java. Код для шагов 1 и 2 такой:

try
{
  dbf = DocumentBuilderFactory.newInstance();
  db = dbf.newDocumentBuilder();
  doc = db.parse(uri);
  if (doc != null)
    generateSAXEvents(doc, this);
}

Далее я пройду через три типа узлов, обрабатываемых классом DomToSax.

Создание событий SAX, продолжение

Сначала рассмотрим обработчик DOCUMENT_NODE:

  case Node.DOCUMENT_NODE:
  {
    dh.startDocument();
    generateSAXEvents(((Document)node).getDocumentElement(), dh);
    dh.endDocument();
    break;
  }

Генерируем событие startDocument, вызываем процедуру рекурсивно для обработки элемента документа, затем генерируем событие endDocument. ELEMENT_NODE обрабатывается похоже на DOCUMENT_NODE, с тем исключением, что вам нужно обработать атрибуты элемента DOM прежде, чем вы сможете генерировать событие startElement. startElement требует, чтобы вы передали объект, который реализует интерфейс Attributes вместе с именем элемента. Вот код:

  case Node.ELEMENT_NODE:
  {
    AttributesImpl saxAttrs = new AttributesImpl();
    if (node.hasAttributes())
    {
      NamedNodeMap attrs = node.getAttributes();
      for (int i = 0; i < attrs.getLength(); i++)
        saxAttrs.addAttribute(null, null,
          attrs.item(i).getNodeName(),
          null, attrs.item(i).getNodeValue());
    }
    dh.startElement(null, null, node.getNodeName(), saxAttrs);
    if (node.hasChildNodes())
    {
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++)
        generateSAXEvents(children.item(i), dh);
    }
    dh.endElement(null, null, node.getNodeName());
    break;
  }

Похожим способом вы обрабатываете узел документа, теперь вы генерируете startElement, вызываете рекурсивно процедуру, затем генерируете событие endElement.

Последний (и самый простой) случай обрабатывает TEXT_NODE. Метод текстового узла getNodeValue возвращает String; преобразуйте его в массив символов (char), затем сгенерируйте событие characters:

  case Node.TEXT_NODE:
  {
    char[] chars = node.getNodeValue().toCharArray();
    dh.characters(chars, 0, chars.length);
    break;
  }

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

Выполнение этого кода при помощи командной строки DomToSax sonnet.xml даст тот результат, которого вы ждете на основании предыдущих примеров. Полный исходный код находится в DomToSax.java.

 

Раздел 4. Манипулирование структурами дерева

Манипулирование деревом DOM

Объектная Модель Документа предоставляет несколько методов для добавления, перемещения и удаления узлов в дереве DOM. Чтобы проиллюстрировать, как это работает, я покажу вам приложение, которое сортирует 14 элементов <line>. Для сортировки этих элементов вам нужно перемещать узлы с одного места на другое в дереве DOM. Поскольку сонет имеет только 14 строк, вы можете использовать пузырьковый алгоритм сортировки для их упорядочивания. Когда этот код будет работать правильно для документа-примера, я покажу вам сокращенный способ. (В качестве упражнения вы можете попробовать сделать этот код более надежным.) Для начала вам нужно получить все элементы <line> в дереве DOM. К счастью, интерфейсы Document и Element содержат метод getElementsByTagName. По данному имени тега этот метод возвращает NodeList со всеми элементами, имеющими такое имя тега. Код такой:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setIgnoringElementContentWhitespace(true);
DocumentBuilder db = dbf.newDocumentBuilder();
doc = db.parse(uri);
if (doc != null)
{
  NodeList theLines = doc.getDocumentElement().
  getElementsByTagName("line");
  sortLines(theLines);
  DomTreePrinter.printNode(doc);
}

Первые несколько строк являются стандартным кодом разбора в DOM, какой вы использовали раньше. Далее getElementsByTagName применяется для получения NodeList элементов <line>; передаваемых затем в метод sortLines. После того, как строки сонета отсортированы, вы применяете класс DomTreePrinter для печати измененного дерева DOM.

Заметьте, что вы не передаете дерево DOM в метод sortLines. Пока ваш объект Document не попадет под сборку мусора, вi можете начать с любого из узлов в NodeList найти предка этого узла, братьев этого узла и потомков этого узла и т.д. Это означает, что вы можете начать с узла, найти его соседний (братский) элемент (следующий элемент <line>) и сравнить текст в этих элементах. Если вам нужно поменять их местами, вы можете использовать функции DOM, чтобы приказать родителю этих узлов поместить один узел перед другим.

...или это дерево DOM манипулирует вами?

Теперь - о более сложных вещах. Концептуально пузырьковая сортировка не сложно; проблема состоит в получении текста данного узла. Вы можете думать, что метод getNodeValue сделает то, что вы хотите, но не в этом случае. В соответствии со стандартом DOM, значение узла Element - null. Чтобы получить текст данного <line> , вам нужно получить все его узлы-потомки типа Text. Когда вы вызываете getNodeValue с узлом Text, то, что вы получаете, и будет текстом, который вы ищете.

Чтобы сделать текст лучше читаемым, используйте метод getTextFromLine для выделения текста, который вам нужен:

public String getTextFromLine(Node lineElement)
{
  StringBuffer returnString = new StringBuffer();
  if (lineElement.getNodeName().equals("line"))
  {
    NodeList kids = lineElement.getChildNodes();
    if (kids != null)
      if (kids.item(0).getNodeType() == Node.TEXT_NODE)
        returnString.append(kids.item(0).getNodeValue());
  }
  else
    returnString.setLength(0);
  return new String(returnString);
developerWorks(r) ibm.com/developerWorks
XML programming in Java technology, Part 3
Page 18 of 30 (c) Copyright IBM Corporation 1994, 2005. All rights reserved.
}

На самом деле этот код - уловка, потому что вы ищете только первый узел-потомок. Это работает для документа-примера, но для более сложных документов вы должны проделать большую работу. (Например, для случая, когда внутри элемента <line> допускается элемент <b>.) Этот код использует метод getNodeName, чтобы убедиться, что это правильный вид элемента, затем он получает потомка этого узла, затем он убеждается, что первый потомок узла - текстовый узел. Предположим, все это так, тогда метод возвращает текст элемента.

Сортировка узлов

Вашей последней задачей является сортировка узлов. Используйте метод String.compareTo, чтобы определить, какая из двух строк является первой в порядке сортировки. Если вам нужно поменять местами два узла, используйте метод insertBefore DOM. Этот метод вставляет один узел перед другим; лучше всего, если узел уже существует в дереве DOM, он просто перемещается в новое место. Вот этот код:

public void sortLines(NodeList theLines)
{
  if (theLines != null)
  {
    int len = theLines.getLength();
    for (int i = 0; i < len; i++)
      for (int j = 0; j < (len - 1 - i); j++)
        if (getTextFromLine(theLines.item(j)).
              compareTo(getTextFromLine(theLines.item(j+1)))
            > 0)
                theLines.item(j).getParentNode().
    insertBefore(theLines.item(j+1),
    theLines.item(j)); 
  }
}

Хотя этот код выглядит сбивающим с толку, он на самом деле не так уж и плох. Циклы for the отрабатывают пузырьковую сортировку. Оператор if сравнивает текст в данной строке с текстом в следующей строке; если вам нужно поменять их местами, вы можете применить insertBefore. Заметьте, что getParentNode применяется для получения предка узла. Если вы имеете предка, вы приказываете предку поместить следующую строку перед текущим узлом.

Полный исходный код см. в DomSorter.java.

Работа с атрибутами

Манипулирование атрибутами узлов в дереве DOM очень похоже на другие функции, которые я рассматривал здесь. Несколько методов DOM работают с атрибутами:

Node.getAttributes()
Если этот Node является Element, метод возвращает NamedNodeMap атрибутов элемента. Если Node является чем-то другим, метод возвращает null.

Element.getAttribute(String name)
Возвращает строковое значение атрибута с заданным именем.

Element.getAttributeNode(String name)
Element.getAttributeNodeNS(String namespaceURI, String name)

Этот метод возвращает объект, который имеет заданное имя (и пространство имен, если оно задано) и реализует интерфейс Attr.

Element.hasAttribute(String name)
Element.hasAttributeNS(String name)

Этот метод возвращает true, если элемент имеет атрибут с заданным именем (и пространством имен, если оно задано).

Element.removeAttribute(String name)
Element.removeAttributeNS(String namespaceURI, String name)

Этот метод удаляет с заданным именем (и пространством имен, если оно задано).

Element.removeAttributeNode(Attr oldAttr)
Это одно из обстоятельств, сбивающих с толку: аргумент для этого метода является объектом, который реализует интерфейс Attr. Вы хотите, чтобы Element удалил атрибут, который соответствует объекту. Метод возвращает объект (Attr), который был удален. Чтобы еще усложнить дело, если атрибут имеет значение по умолчанию (иными словами, если атрибут имеет значение независимо от того, задано оно в XML-документе или нет), удаленный атрибут заменяется на новый Attr, который имеет значение по умолчанию.

Element.setAttribute(String name, String value)
Element.setAttributeNS(String namespaceURI, String name, String value)

Этот метод добавляет новый атрибут с заданным именем и значением (и пространством имен, если оно задано).

Element.setAttributeNode(Attr newAttribute)
Element.setAttributeNodeNS(Attr newAttribute)

Этот метод добавляет объект Attr, переданный в него в качестве аргумента. Если новый атрибут заменяет уже существующий с тем же именем (и пространством имен, если оно задано), этот метод возвращает замененный объект, иначе метод возвращает null.

Кодовый пример DomAttributes.java разбирает XML-файл, затем он применяет метод Element.setAttribute для добавления атрибута к каждому узлу Element в дереве DOM. Его последняя задача - использование класса DomTreePrinter для печати модифицированного дерева DOM.

Манипулирование деревом JDOM

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

Код начинается со сканирования командной строки, а затем вызова метода parseAndSortLines() для разбора и обработки сонета:

public static void main(String[] argv)
{
  if (argv.length == 0 ||
     (argv.length == 1 &&argv[0].equals("-help")))
  // print message and exit
  JdomSorter js = new JdomSorter();
  js.parseAndSortLines(argv[0]);
}
public void parseAndSortLines(String uri)
{
  try
  {
    SAXBuilder sb = new SAXBuilder();
    Document doc = sb.build(new File(uri));
    sortLines(doc);

Метод sortLines() - то место, где выполняется большая часть работы. Передайте весь ваш документ в этот метод; его первой задачей является получение всех элементов <line>.

Когда вы работали с DOM, вы применяли getElementsByTagName() для нахождения всех элементов, которые вам нужны. Если вы имеете эти элементы, вы можете использовать родительский элемент (доступный через getParentNode()) для перемещения <line>, как вам нужно.

Еще о манипуляциях в JDOM

Эквивалентом getElementsByTagName() в JDOM являются getChild() и getChildren(). Главная концептуальная разница между JDOM и DOM состоит в том, что JDOM работает только с непосредственными "детьми" элемента, а не с его отдаленными потомками. Другими словами, вы не можете начать с корня документа и запросить все элементы <line> в документе; вы должны найти элемент <lines> и запросить все его дочерние элементы <line>.

Поскольку вы знаете структуру вашего XML-документа, вы можете получить элемент <lines> очень быстро. Вот как это сделать:

public void sortLines(Document sonnet)
{
  Element linesElement = sonnet.getRootElement().
  getChild("lines");
  List lines = linesElement.getChildren("line");

Для данного объекта Document запросите корневой элемент (<sonnet>), затем запросите его дочерний элемент с именем <lines>. Далее запрашивайте для всех элементов <line> дочерний элемент. Если вы не знаете точной структуры документа, вы должны применять метод getChildren() для получения дочерних элементов, выбирать подходящий дочерний элемент, а затем применять getChildren(), пока не найдете тот элемент, который вам нужен.

Заметьте, что JDOM возвращает List, часть Java Collections API. Одна из многих замечательных вещей, относящихся к интерфейсу List, состоит в том, что любые изменения, которые вы сделаете в List, отражаются в обеспечивающей его базовой структуре.

В JDOM процедура пузырьковой сортировки такая:

for (int i = 0; i < 14; i++)
  for (int j = 0; j < (14 - 1 - i); j++)
    if ( ((Element)lines.get(j)).getText().
            compareTo(((Element)lines.get(j+1)).getText())
        > 0)
      lines.add(j, lines.remove(j+1));

Пару вещей стоит здесь упомянуть. Прежде всего, заметьте, что JDOM предоставляет вам метод getText() для получения текста данного элемента. Это значит, что вам не нужно писать вспомогательную процедуру для получения текста элемента, дочернего для данного элемента, как вы делали в DOM. Также заметьте, что вы должны преобразовать тип составляющей из List в Element.

Когда вам нужно поменять местами две соседние строки, вы используете совместно методы add() и remove(). Удаляете строку на позиции j+1, затем вставляете ее на позицию j.

Одно последнее замечание: поскольку интерфейс List дает вам возможность модифицировать обеспечивающие его базовые данные, ваш метод sortLines() не возвращает ничего. Изменения, которые вы делаете в элементе <lines>, отражаются в самом объекте Document.

Вывод результатов

Теперь, когда вы отсортировали строки сонета, вашей последней задачей является вывести их. Вы применяете XMLOutputter с некоторыми его свойствами, о которых я раньше не упоминал:

  sortLines(doc);
  XMLOutputter xo = new XMLOutputter();
  xo.setTrimAllWhite(true);
  xo.setIndent(" ");
  xo.setNewlines(true);
  xo.output(doc, System.out);

Прикажите при помощи метода setTrimAllWhite(), чтобы XMLOutputter удалял все лишние пропуски, затем используйте setIndent() и setNewlines(), чтобы установить красивую печать вашего XML-документа. Результаты выглядят так:

C:\adv-xml-prog>java JdomSorter sonnet.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sonnet SYSTEM "sonnet.dtd">
<sonnet type="Shakespearean">
 <author>
. . .
 <lines>
  <line>And in some perfumes is there more delight</line>
  <line>And yet, by Heaven, I think my love as rare</line>
  <line>As any she belied with false compare.</line>
  <line>But no such roses see I in her cheeks.</line>
  <line>Coral is far more red than her lips red.</line>
. . .

Полный исходный код см. в JdomSorter.java.

Последнее замечание о манипуляции деревом

Хотя это и лежит вне сферы рассмотрения нашего учебника, знайте, что простейшим способом отсортировать строки сонета является таблица стилей XSLT. XSLT предоставляет великолепный элемент <xsl:sort>, который делает все, что вы с таким трудом делали в этом примере. Вот часть таблицы стилей:

<xsl:template match="lines">
 <lines>
  <xsl:for-each select="line">
   <xsl:sort/>
   <xsl:copy>
    <xsl:apply-templates select="*|@*|text()"/>
   </xsl:copy>
  </xsl:for-each>
 </lines>
</xsl:template>

Я не хочу подробно объяснять эту таблицу стилей, но этот шаблон делает для вас всю работу по сортировке. Когда вы находите элемент <lines>, вы выводите новый элемент <lines>, сортируете все элементы <line> внутри него, а затем копируете их в результирующий документ.

Вы можете увидеть полную таблицу стилей в sonnetSorter.xsl.

 

Раздел 5. Продвинутые свойства DOM

Сериализация дерева DOM

До сих пор вы применяли в примерах DOM printDomTree. Теперь я покажу вам два других способа вывести (или сериализовать) дерево DOM. Первый подход состоит в простом перемещении printDomTree в отдельный класс, так что вы не должны включать этот метод в исходный Java-код каждого DOM-приложения, которое вы создаете.

Второй подход состоит в использовании класса DOMSerializer. Этот класс является частью DOM Уровня 3, но в настоящее время он не добавлен к классам фабрик JAXP. (DOMSerializer, между прочим, может быть добавлен или не добавлен в JAXP в будущем.)

Для первого подхода просто создайте пакет с именем com.ibm.dw.xmlprogjava и класс в пакете с именем DomTreePrinter. Этот класс содержит один public static метод с именем printNode. Вот как выглядит его код:

package com.ibm.dw.xmlprogjava;
. . .
public class DomTreePrinter
{
/** Prints the specified node, recursively. */
public static void printNode(Node node)
{
  int type = node.getNodeType();
  switch (type)
  {
    // print the document element
    case Node.DOCUMENT_NODE:
    {
      System.out.println("<?xml version=\"1.0\" ?>");
      printNode(((Document)node).getDocumentElement());
      break;
    }

Единственное изменение состоит в переименовании метода в printNode вместо printDomTree. Полный исходный код см. в DomThree.java и DomTreePrinter.java.

Дальше вы увидите, как создать DOMSerializer.

Применение LSSerializer

Второй способ записать дерево DOM как XML-документ состоит в применении интерфейса LSSerializer. Он является частью пакета org.w3c.dom.ls, который является частью спецификации DOM Уровня 3 Загрузка и Сохранение. Поддержка этого пока незаконченного стандарта в Xerces, возможно, изменится, но код, который работает в июле 2004 г. такой:

import org.apache.xerces.dom.DOMOutputImpl;
import org.w3c.dom.Document;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSParser;
import org.w3c.dom.ls.LSSerializer;
. . .
public class DomFour
{
  public void parseAndPrint(String uri)
  {
    Document doc = null;
    try
    {
      System.setProperty(DOMImplementationRegistry.PROPERTY,
        "org.apache.xerces.dom.DOMXSImplementationSourceImpl");
      DOMImplementationRegistry direg =
        DOMImplementationRegistry.newInstance();
      DOMImplementationLS dils =
         (DOMImplementationLS) direg.getDOMImplementation("LS");
      LSParser lsp = dils.createLSParser
         (DOMImplementationLS.MODE_SYNCHRONOUS, null);
      doc = lsp.parseURI(uri);
      LSSerializer domWriter = dils.createLSSerializer();
      LSOutput lso = new DOMOutputImpl();
      lso.setByteStream(System.out);
      domWriter.write(doc, lso);
    }
    catch (Exception e)
    {
      System.err.println("Sorry, an error occurred: " + e);
    }
}

Этот код использует несколько специфичных для Xerces реализаций интерфейсов DOM Уровня 3. На июль 2004 г., этот код компилируется и выполняется только со специальной пристройкой к парсеру Xerces. По мере развития стандарта DOM Уровня 3 реализация Xerces будет становиться более зрелой, и этот код почти определенно изменится. В приведенном выше коде классы DOMXSImplementationSourceImpl и DOMOutputImpl специфичны для парсера Xerces. Полный исходный код см. в DomFour.java.

Другие функцииDOM

Хотя в примере сортировки сонета они вам не нужны, но есть несколько других методов DOM, полезных для манипуляции деревьями DOM. Наиболее часто применяются такие методы:

appendChild(Node newChild)
Добавляет узел newChild как последний дочерний узел родительского узла.

removeChild(Node oldChild)
Удаляет узел oldChild из родительского узла.

replaceChild(Node newChild, Node oldChild)
Заменяет oldChild на newChild. (Примечание: и newChild, и oldChild должны быть созданы одним DocumentBuilder. )

Использование разных DOM-парсеров

Во всех примерах этого учебника вы использовали парсер Xerces. Однако, большинство примеров может использовать любой другой JAXP-совместимый парсер без каких-либо изменений. (Единственное исключение - пример DOMSerializer, который использует классы DOM Уровня 3, еще не стандартизированные в JAXP.) Одной из целей JAXP является обеспечение для вас возможности менять парсеры без каких-либо изменений в вашем исходном коде. JAXP достигает этого загрузкой определенного парсер во время выполнения. Во время выполнения JAXP определяет имя класса, который реализует интерфейс DocumentBuilderFactory. JAXP ищет имя класса в четырех местах (именно в таком порядке):

  1. Значение свойства javax.xml.parsers.DocumentBuilderFactory
  2. Значение свойства javax.xml.parsers.DocumentBuilderFactory в файле jre/lib/jaxp.properties
  3. Проверяя все JAR-файлы в CLASSPATH, первое имя, найденное в файле META-INF/services/javax.xml.parsers.DocumentBuilderFactory
  4. DocumentBuilderFactory по умолчанию для платформы Java (в JDK 1.4, парсер по умолчанию -org.apache.crimson.jaxp.DocumentBuilderFactoryImpl)

Вы можете задать свойство javax.xml.parsers.DocumentBuilderFactory двумя способами. Первый состоит в использовании параметра -D командной строки:

java -Djavax.xml.parsers.DocumentBuilderFactory=[DBF class] . . .

Второй состоит в добавлении следующих строк в ваш исходный Java-код перед созданием DocumentBuilderFactory:

System.setProperty("javax.xml.parsers.DocumentBuilderFactory",
              "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

 

Раздел 6. Продвинутые свойства SAX

Останов SAX-парсера

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

В вашем приложении, убивающем парсер, вы будете искать четвертый элемент <line> в сонете. Когда вы его найдете, вы создадите SAXParseException, а затем вызовете метод fatalError. Я далее покажу вам этот код.

Поиск

Поскольку события SAX не сохраняют состояния, ваш код должен отслеживать все события, которые вы увидите. Если вы хотите найти четвертый элемент <line>, вы знаете, что он начинается с четвертого события startElement, для которого rawName - line. Все события characters и ignorableWhitespace, которые ваш парсер парсер получает, пока он находится в четвертом элементе <line>, являются частью текста элемента, и когда он находит четвертое событие endElement, для которого rawName - line, вы знаете, что у вас есть уже все, что вы искали. В этот момент создавайте ваше исключение и вызывайте fatalError.

Прежде всего, используем три переменные для поддержания информации о состоянии:

int lineCount = 0;
boolean inFourthLine = false;
StringBuffer fourthLine = new StringBuffer();

Используйте lineCount для подсчета того, сколько элементов <line> вы увидели до сих пор, установите флажок inFourthLine, когда вы увидите событие startElement для четвертого элемента <line>, используйте fourthLine для сохранения текста самого элемента <line>.

Метод startElement выглядит так:

public void startElement(String namespaceURI, String localName,
                String rawName, Attributes attrs)
{
  if (rawName.equals("line") && ++lineCount == 4)
    inFourthLine = true;
}

Далее я покажу вам другие обработчики событий.

Обработка других событий

Теперь кратко рассмотрим другие обработчики событий, которые использует "убийца парсера". Прежде всего, обработчики characters и ignorableWhitespace - которые просто проверяют флаг inFourthLine и сохраняют текст, если парсер находится в четвертом элементе <line>:

public void characters(char ch[], int start, int length)
{
  if (inFourthLine)
    fourthLine.append(new String(ch, start, length));
}
public void ignorableWhitespace(char ch[], int start, int length)
{
  if (inFourthLine)
    characters(ch, start, length);
}

Теперь обработчик для endElement. Когда ваш парсер достигает конца четвертого элемента<line>, отпечатайте текст строки, а затем создайте исключение, которое убьет парсер:

public void endElement(String namespaceURI, String localName,
                    String rawName)
        throws SAXException
{
  if (rawName.equals("line") && inFourthLine)
  {
    System.out.println("\nThe text of the fourth line is: \n");
    System.out.println("\t" + fourthLine);
    SAXParseException spe =
      new SAXParseException("Found the fourth <line>, " +
        "so we killed the parser!",
        new LocatorImpl());
    fatalError(spe);
  }
}

При выполнении SaxKiller вы должны увидеть что-то такое:

C:\xml-prog-java>java SaxKiller sonnet.xml
The text of the fourth line is:
If hairs be wires, black wires grow on her head.

Исключение не выведено, поскольку вы catch его в методе main:

SaxKiller s1 = new SaxKiller();
try
{
  s1.parseURI(argv[0]);
}
// We're expecting an exception, so we ignore
// anything that happens...
catch (Exception e) { }

Полный листинг см. в SaxKiller.java.

Использование разных SAX-парсеров

Как вы видели ранее, JAXP дает вам возможность задавать разные DOM-парсеры во время выполнения; он также дает вам возможность задавать во время выполнения и реализацию SAXParserFactory. Во время выполнения JAXP определяет имя класса, который реализует интерфейс SAXParserFactory. Порядок, в котором JAXP ищет имя класса, следующий:

  1. Значение свойства javax.xml.parsers.SAXParserFactory
  2. Значение свойства javax.xml.parsers.SAXParserFactory в файле jre/lib/jaxp.properties
  3. Поиск во всех JAR-файлах в CLASSPATH, первое имя, найденное в файле META-INF/services/javax.xml.parsers.SAXParserFactory
  4. SAXParserFactory по умолчанию для платформы Java (в JDK 1.4 парсер по умолчанию -
org.apache.crimson.jaxp.SAXParserFactory)

Вы можете задать свойство javax.xml.parsers.SAXParserFactory двумя способами. Первый состоит в применении параметра командной строки -D:

java -Djavax.xml.parsers.SAXParserFactory=[SPF class name] . . .

Второй состоит в добавлении следующих строк в ваш исходный Java-код перед созданием SAXParserFactory:

System.setProperty("javax.xml.parsers.SAXParserFactory",
  "org.apache.xerces.jaxp.SAXParserFactoryImpl");
SAXParserFactory spf = SAXParserFactory.newInstance();

Этот код приказывает JAXP использовать парсер Xerces от Apache Software Foundation.

 

Раздел 7. Резюме и ссылки

Резюме

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

Ресурсы

Просмотрите предыдущие учебники этой серии:

Посетите страницу страницу технических отчетов DOM в W3C, если вас интересуют ссылки на все, относящееся к DOM. Отдельные спецификации см. в:

Прочитайте о SAX Version 2.0.

Изучите все о JDOM на домашней странице проекта JDOM.

Если вы хотите освежить свои познания о самом XML, прочитайте популярный учебник "Introduction to XML" (developerWorks, август 2002). Перевод на нашем сайте - "Введение в XML.

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

Наконец, посмотрите, как вы можете стать Сертифицированным в IBM разработчиком в XML и родственных технологиях (http://www-1.ibm.com/certify/certs/adcdxmlrt.shtml).

 

Об авторе

Doug Tidwell

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

Также доступен слабоактивный Doug Tidwell (Doug Tidwell SA), который прописывается для приема небольшими дозами через каждые 24 часа. Побочными эффектами от Doug Tidwell обычно были легкими и включали в себя головокружение, тошноту - от умеренной до сильной, нечувствительность в конечностях, в редких случаях - паралич и смерть.

Попросите у вашего врача Doug Tidwell, если он подходит для вас.

Подробности см. в описании препарата Doug.


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