Если необходимо послать кого-то купить литр молока, что проще сказать этому человеку? "Пожалуйста, сходи купи литр молока" или "Выйди из дома через переднюю дверь. Поверни налево. Пройди три квартала. Поверни направо. Пройди половину квартала. Поверни направо и войди в магазин. Подойди к четвертому стеллажу. Пройди пять метров вдоль стеллажа. Поверни налево. Возьми литровый пакет молока. Принеси его к кассе. Заплати за молоко. Затем возвращайся домой". Это нелепо! Большая часть взрослых людей достаточно умна, чтобы самостоятельно купить молоко, довольствуясь лишь инструкцией типа "Пожалуйста, сходи купи литр молока".
Языки запросов и компьютерный поиск очень похожи. Проще сказать "Найди копию Криптономикона", чем описать детальную логику поиска в некоторой базе данных. Так как операции поиска имеют одинаковую логику, можно разработать обобщенный язык, который позволит задавать выражения вида "Найти все книги, написанные Нилом Стивенсоном", а затем создать механизм обработки таких запросов для конкретных баз данных.
Среди всех языков запросов Structured Query Language (SQL) — это язык, разработанный и оптимизированный для поиска в определенных видах реляционных баз данных. Список других, менее известных языков запросов включает в себя Object Query Language (OQL) и XQuery. Однако предметом рассмотрения этой статьи является XPath, язык запросов, разработанный для поиска информации в XML-документах. Например, простой запрос на языке Xpath для поиска документе названия всех книг, автором которых является Нил Стивенсон (Neal Stephenson), выглядит примерно так:
//book[author="Neal Stephenson"]/title |
Для сравнения, код для выполнения аналогичного действия с использованием «чистой» DOM модели выглядит как в листинге 1:
Листинг 1. Код, использующий DOM, для поиска названий книг, написанных Нилом Стивенсоном
ArrayList result = new ArrayList();
NodeList books = doc.getElementsByTagName("book");
for (int i = 0; i < books.getLength(); i++) {
Element book = (Element) books.item(i);
NodeList authors = book.getElementsByTagName("author");
boolean stephenson = false;
for (int j = 0; j < authors.getLength(); j++) {
Element author = (Element) authors.item(j);
NodeList children = author.getChildNodes();
StringBuffer sb = new StringBuffer();
for (int k = 0; k < children.getLength(); k++) {
Node child = children.item(k);
// really should to do this recursively
if (child.getNodeType() == Node.TEXT_NODE) {
sb.append(child.getNodeValue());
}
}
if (sb.toString().equals("Neal Stephenson")) {
stephenson = true;
break;
}
}
if (stephenson) {
NodeList titles = book.getElementsByTagName("title");
for (int j = 0; j < titles.getLength(); j++) {
result.add(titles.item(j));
}
}
}
|
Хотите верьте – хотите нет, но DOM-код в листинге 1 все-таки не так хорош и изящен, как простое выражение на языке XPath. Какой код проще и удобнее написать, отладить и сопровождать? По-моему, ответ очевиден!
Однако как бы он не был выразителен, XPath – это все-таки не язык Java, фактически, XPath даже не является языком программирования. Существует очень много вещей, которые невозможно выполнить с помощью XPath, включая даже запросы, которые нельзя сформировать. Например, XPath не может найти все книги, для которых указан некорректный International Standard Book Number (ISBN) или найти всех авторов, для которых сторонняя база данных показывает, что авторские гонорары начисляются в текущий момент. К счастью, можно интегрировать XPath в программы, написанные на Java, а значит, получить все лучшее из двух миров: Java — для всех тех вещей, с которыми хорошо справляется Java, и XPath для всего того, с чем хорошо справляется Xpath.
До сих пор набор интерфейсов программирования приложений (API), с помощью которого Java-программа выполняла XPath-запросы, зависел от типа реализации XPath. Xalan имел один набор API, Saxon — другой, а остальные реализации имели полностью собственные наборы API. Это значило, что код оказывался привязан к одному продукту. В общем случае хотелось бы без особых трудностей и переписывания кода провести эксперименты с различными реализациями по определению характеристик эффективности.
По этой причине в Java 5 включен пакет javax.xml.xpath — он предоставляет реализацию и объектно-независимую модель библиотеки XPath. Этот пакет также доступен в Java версии 1.3 и выше, если дополнительно установить Java API for XML Processing (JAXP) 1.3. Из числа сторонних продуктов Xalan 2.7 и Saxon 8 также включают в себя реализацию этой библиотеки.
Сейчас будет продемонстрировано как это работает на практике. Также будут подробнее рассмотрены некоторые детали. Предполагается, что для запросов есть список книг, в котором могут быть найдены книги, написанные Нилом Стивенсоном. Полагается также, что список книг имеет такой же вид, как в листинге 2:
Листинг 2. XML-документ, содержащий информацию о книгах
<inventory>
<book year="2000">
<title>Snow Crash</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553380958</isbn>
<price>14.95</price>
</book>
<book year="2005">
<title>Burning Tower</title>
<author>Larry Niven</author>
<author>Jerry Pournelle</author>
<publisher>Pocket</publisher>
<isbn>0743416910</isbn>
<price>5.99</price>
</book>
<book year="1995">
<title>Zodiac</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553573862</isbn>
<price>7.50</price>
</book>
<!-- more books... -->
</inventory> |
Запрос XPath, который выбирает все необходимые книги из документа, достаточно прост: //book[author="Neal Stephenson"]. Чтобы найти только названия этих книг, требуется всего лишь небольшое изменение запроса — выражение будет выглядеть следующим образом: //book[author="Neal Stephenson"]/title.
Наконец, найдем то, что действительно необходимо — это текст узла-потомка элемента title. Для этого требуется еще одно небольшое изменение, полное выражение будет следующим: //book[author="Neal Stephenson"]/title/text().
Теперь рассмотрим программу, которая использует этот поисковый запрос в программе на языке Java и выводит затем названия всех найденных книг. Прежде всего необходимо загрузить документ в объект Document. Для простоты, будем полагать, что список книг содержится в файле books.xml в текущем каталоге. Ниже дан простой фрагмент кода, который обрабатывает документ и создает соответствующий объект Document:
Листинг 3. Обработка документа с помощью JAXP
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true); // never forget this!
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse("books.xml"); |
Итак, это всего лишь стандартные JAXP и DOM, ничего нового.
Теперь создаем XPathFactory:
XPathFactory factory = XPathFactory.newInstance(); |
Затем используется эта фабрика для создания объекта XPath:
XPath xpath = factory.newXPath(); |
XPath компилирует XPath-выражение:
XPathExpression expr = xpath.compile("//book[author='Neal Stephenson']/title/text()"); |
Наконец, необходимо выполнить запрос XPath для получения результата. Выражение исполняется по отношению к определенному по контексту узлу, которым в данном случае является весь документ. Также необходимо явно задать тип возвращаемого результата. В примере тип NODE задается как тип результата исполнения выражения:
Object result = expr.evaluate(doc, XPathConstants.NODESET); |
Теперь можно сохранить результат в DOM NodeList, затем итеративно обратиться к каждому элементу, чтобы найти все названия:
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
} |
В листинге 4 весь этот код собран в одну программу. Необходимо также отметить, что использование описанных методов может вызвать несколько исключений, которые нужно объявить в пункте throws, обеспечить компиляцию кода, представленный выше:
Листинг 4. Программа для поиска в XML-документе с помощью фиксированного XPath выражения
import java.io.IOException;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
public class XPathExample {
public static void main(String[] args)
throws ParserConfigurationException, SAXException,
IOException, XPathExpressionException {
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true); // never forget this!
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse("books.xml");
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr
= xpath.compile("//book[author='Neal Stephenson']/title/text()");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
}
} |
При совместной разработке на двух различных языках программирования, таких XPath и Java, нужно ожидать некоторых заметных "швов" там, где соединяются два языка программирования. Не все совместится достаточно хорошо. XPath и Java имеют различную систему типов данных. В частности XPath 1.0 имеет только четыре основных типа данных:
- node-set
- number
- boolean
- string
Java, безусловно, имеет значительно большее количество возможных типов данных, включая объектные типы данных, определяемые пользователем.
Многие выражения XPath, особенно запрашивающие отдельные элементы XML-документа, возвращают результат типа node-set. Однако есть также другие возможности. Например выражение на языке XPath count(//book) возвращает количество книг в документе. Результат работы другого выражения на языке XPath count(//book[@author="Neal Stephenson"]) > 10
имеет тип результата boolean: возвращаемое значение равно true, если больше десяти книг Нила Стивенсона содержится в документе, и false, если десять и менее.
Метод evaluate() возвращает выражение типа Object. . Что именно будет результатом зависит от выполнения XPath-выражения, также как и тип запрашиваемых данных. В общем случае, XPath
- number cоответствует в
java.lang.Double - string cоответствует в
java.lang.String - boolean cоответствует в
java.lang.Boolean - node-set cоответствует в
org.w3c.dom.NodeList
Когда выполняется XPath выражение в Java, второй аргумент определяет тип возвращаемого параметра. Существует пять возможных типов возвращаемых параметров, все они имеют определены как константы в классе javax.xml.xpath.XPathConstants со следующими именами:
-
XPathConstants.NODESET -
XPathConstants.BOOLEAN -
XPathConstants.NUMBER -
XPathConstants.STRING -
XPathConstants.NODE
Необходимо заметить, что значение XPathConstants.NODE не является в действительности типом данных XPath. Это значение используется только тогда, когда нет уверенности, что выражение на XPath вернет значение, состоящее из единственного узла или когда не требуется более одного значения. Если выражение на XPath возвращает более одного узла и был определен тип возвращаемого параметра как XPathConstants.NODE, то evaluate() вернет первый найденный в документе узел, удовлетворяющий заданному выражению. Если выражение на языке XPath должно возвратить пустой список и был определен тип возвращаемого параметра XPathConstants.NODE, тогда метод evaluate() вернет null.
Если запрашиваемое преобразование типа не может быть сделано, то метод evaluate() сбрасывает исключительную ситуацию типа XPathException.
Если элементы XML-документа используют пространство имен, то выражение XPath для запроса данных из этого документа должно использовать то же пространство имен. XPath-выражению не требуется иметь те же префиксы, нужно только использовать URI того же пространства имен. Когда XML-документ использует пространство имен по умолчанию, выражение XPath должно использовать префикс, даже если целевой документ его не использует.
Однако Java-программы — это не XML-документы, поэтому обычное разрешение пространства имен не применяется. Вместо этого необходимо использовать объект, который отображает префиксы в пространство имен URI. Этот объект — экземпляр интерфейса javax.xml.namespace.NamespaceContext . Например, предположим, что документ содержит информацию о книгах в пространстве имен http://www.example.com/books, как в листинге 5:
Листинг 5. XML-документ использует пространство имен по умолчанию
<inventory xmlns="http://www.example.com/books">
<book year="2000">
<title>Snow Crash</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553380958</isbn>
<price>14.95</price>
</book>
<!-- more books... -->
</inventory> |
Выражение XPath, которое находит названия всех книг Нила Стивенсона теперь становится вот таким: //pre:book[pre:author="Neal Stephenson"]/pre:title/text(). Однако необходимо отобразить префикс pre to the URI http://www.example.com/books. в URI http://www.example.com/books. Немного странно, что интерфейс NamespaceContext не имеет реализации по умолчанию в комплекте разработчика Java (JDK) или JAXP, но это так. Тем не менее, не так уж сложно сделать такую реализацию самостоятельно. В листинге 6 показана простая реализация для одного пространства имен. Необходимо также отобразить префикс xml.
Листинг 6. Простой контекст для связывания одного пространства имен с пространством имен, используемым по умолчанию
import java.util.Iterator;
import javax.xml.*;
import javax.xml.namespace.NamespaceContext;
public class PersonalNamespaceContext implements NamespaceContext {
public String getNamespaceURI(String prefix) {
if (prefix == null) throw new NullPointerException("Null prefix");
else if ("pre".equals(prefix)) return "http://www.example.org/books";
else if ("xml".equals(prefix)) return XMLConstants.XML_NS_URI;
return XMLConstants.NULL_NS_URI;
}
// This method isn't necessary for XPath processing.
public String getPrefix(String uri) {
throw new UnsupportedOperationException();
}
// This method isn't necessary for XPath processing either.
public Iterator getPrefixes(String uri) {
throw new UnsupportedOperationException();
}
} |
Не так уж сложно использовать отображение для привязывания контекстов имен и префиксов, а также для добавления методов, которые позволяют сделать контекст пространства имен многократно используемым.
После создания объекта NamespaceContext необходимо установить его в объект XPath перед компиляцией выражения. Теперь можно использовать в запросах те же префиксы, что и раньше. Например:
Листинг 7. Запрос XPath, который использует пространства имен
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
xpath.setNamespaceContext(new PersonalNamespaceContext());
XPathExpression expr
= xpath.compile("//pre:book[pre:author='Neal Stephenson']/pre:title/text()");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
} |
В некоторых случаях полезно определить расширения функций на языке Java для использования вместе с выражениями на XPath. Эти функции могут выполнять задачи, которые сложны или даже невозможны при использовании чистого XPath. Однако они должны быть настоящими функциями, а не просто произвольными методами. То есть они не должны иметь побочных эффектов. (Функции XPath могут быть вычислены в любом порядке и произвольное число раз).
Функции расширения, реализуемые через Java XPath API должны реализовывать интерфейс javax.xml.xpath.XPathFunction. Этот интерфейс определяет единственный метод evaluate():
public Object evaluate(List args) throws XPathFunctionException |
Этот метод должен возвратить значение одного из пяти типов в языке Java, которые могут быть конвертированы в типы данных XPath:
-
String -
Double -
Boolean -
Nodelist -
Node
В листинге 8 показана функция расширения, которая проверяет контрольную сумму кода ISBN и возвращает значение типа Boolean.
Основное правило для подсчета контрольной суммы заключается в том, что первые девять цифр умножаются на свои позиции (то есть первая цифра умножается на единицу, вторая — на два и так далее). Полученные значения складываются и высчитывается остаток от деления на одиннадцать. Если остаток равен десяти, то последняя цифра равна X.
Листинг 8. Функция расширения XPath для проверки кода ISBN
import java.util.List;
import javax.xml.xpath.*;
import org.w3c.dom.*;
public class ISBNValidator implements XPathFunction {
// This class could easily be implemented as a Singleton.
public Object evaluate(List args) throws XPathFunctionException {
if (args.size() != 1) {
throw new XPathFunctionException("Wrong number of arguments to valid-isbn()");
}
String isbn;
Object o = args.get(0);
// perform conversions
if (o instanceof String) isbn = (String) args.get(0);
else if (o instanceof Boolean) isbn = o.toString();
else if (o instanceof Double) isbn = o.toString();
else if (o instanceof NodeList) {
NodeList list = (NodeList) o;
Node node = list.item(0);
// getTextContent is available in Java 5 and DOM 3.
// In Java 1.4 and DOM 2, you'd need to recursively
// accumulate the content.
isbn= node.getTextContent();
}
else {
throw new XPathFunctionException("Could not convert argument type");
}
char[] data = isbn.toCharArray();
if (data.length != 10) return Boolean.FALSE;
int checksum = 0;
for (int i = 0; i < 9; i++) {
checksum += (i+1) * (data[i]-'0');
}
int checkdigit = checksum % 11;
if (checkdigit + '0' == data[9] || (data[9] == 'X' && checkdigit == 10)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
} |
Следующий шаг — это сделать функцию расширения доступной для Java-программы. Для этого необходимо установить javax.xml.xpath.XPathFunctionResolver в объект XPath перед компиляцией выражения. Преобразователь функции отображает XPath-имя и URI пространства имен для функции в Java-класс и реализует функцию. В
листинге 9 приведен пример простого преобразователя функции, который отображает функцию расширения valid-isbn с пространством имен http://www.example.org/books в класс из листинга 8.
Например, выражение на XPath //book[not(pre:valid-isbn(isbn))] находит все книги, чьи коды ISBN не проходят проверку на контрольную сумму.
Листинг 9. Контекст функции-расширения valid-isbn
import javax.xml.namespace.QName;
import javax.xml.xpath.*;
public class ISBNFunctionContext implements XPathFunctionResolver {
private static final QName name
= new QName("http://www.example.org/books", "valid-isbn");
public XPathFunction resolveFunction(QName name, int arity) {
if (name.equals(ISBNFunctionContext.name) && arity == 1) {
return new ISBNValidator();
}
return null;
}
} |
Так как функция-расширение должна находиться в пространстве имен, необходимо использовать объект NamespaceResolver при вычислении выражения, содержащего функцию расширения, даже если документ, в котором проводится поиск не использует пространство имен. Так как XPathFunctionResolver, XPathFunction и NamespaceResolver являются интерфейсами, то можно использовать их в одном классе, если это удобно.
Значительно проще писать запросы на декларативных языках программирования типа SQL и XPath, чем на императивных языках, таких как Java и C. Значительно проще реализовывать сложную логику на полных по Тьюрингу языках, таких как Java и C, чем на декларативных языках наподобие SQL и XPath. К счастью, можно совместить императивный и декларативный языки, используя такой набор API, как Java Database Connectivity (JDBC) и javax.xml.xpath. Все больше и больше данных в мире переводится в формат XML, поэтому javax.xml.xpath становится таким же важным компонентом, каким уже стал java.sql.
Научиться
- Оригинал статьи: The Java XPath API (EN).
- "Get started with XPath 2.0" (EN): используя возросшую силу и эффективность XPath 2.0, научитесь создавать просто очень сложные запросы с помощью новой модели данных.
- "Working with JAXP namespace contexts"(EN): Норм Уолш обосновывает использование пространства имен.
-
XML in a Nutshell
(Elliotte Rusty Harold and W. Scott Means, O'Reilly, 2005): прочтите это полное руководство по XML, содержащее описание XPath 1.0, DOM и JAXP.
- Раздел
Технология Java сайта developerWorks Россия: откройте для себя сотни статей, содержащих информацию обо всех аспектах программирования на Java.
-
Сертификация IBM XML 1.1: узнайте как стать сертифицированным разработчиком IBM по XML 1.1 и сопутствующим технологиям (EN).
-
XML:обратитесь к разделу XML developerWorks Россия, чтобы найти широкий спектр технических статей и руководств, стандартов, советов и книг IBM Redbook.
-
Технические семинары и интернет-конференции developerWorks Россия: будьте в курсе современных технологий.
Получить продукты и технологии
-
Проект JAXP: скачайте JAXP 1.3 для Java 1.3 и 1.4 с java.net (EN).
-
Xalan 2: узнайте о движке XSLT, поддерживающем XPath API (о котором рассказывается в этой статье) и разрабатываемом проектом Apache Project (EN).
-
SAXON 8: попробуйте движок XSLT, создателем которого является Michael Kay. Этот движок также поддерживает XPath API, описанный в этой статье (EN).
-
IBM trial software: создайте свой следующий проект с помощью программного обеспечения, которое можно загрузить непосредственно с сайта developerWorks (EN).
