Язык XML играет важную роль во многих областях, в том числе при хранении и поиске информации, в издательском деле и при передаче данных по сетям; в данной главе мы научимся работать с XML в С++. Поскольку эта книга больше посвящена С++, чем XML, я полагаю, вы уже имеете некоторый опыт применения различных технологий, связанных с XML, например SAX, DOM, XML Schema, XPath и XSLT. He стоит беспокоиться из-за того, что вы не являетесь экспертом во всех этих областях; приводимые в данной главе рецепты достаточно независимы друг от друга, поэтому вы можете пропустить некоторые из них и все-таки понять остальные. Во всяком случае, каждый рецепт дает краткое описание концепций XML и используемого ими инструментария.
Если вы имеете достаточный опыт программирования на другом языке, например на Java, вы можете предположить, что средства обработки документов XML входят в состав стандартной библиотеки С++. К сожалению, XML делал только первые шаги, когда стандарт C++ был уже принят, и хотя добавление средств обработки документов XML в новую версию стандартной библиотеки C++ вызывает большой интерес, в настоящее время вам придется полагаться на несколько доступных в C++ великолепных библиотек XML, поставляемых независимыми разработчиками.
Возможно, перед началом чтения рецептов вы захотите скачать из Интернета и установить библиотеки, которые будут рассмотрены в настоящей главе. В табл. 14.1 приводятся домашние страницы каждой библиотеки, а в табл. 14.2 указано назначение каждой библиотеки и ссылки на рецепты, в которых они используются. В таблицах не указаны точные версии различных спецификаций и рекомендаций XML, реализованные каждой библиотекой, поскольку эта информация, вероятно, изменится в ближайшем будущем.
Табл. 14.1. Библиотеки C++ для XML
Имя библиотеки | Домашняя страница |
---|---|
TinyXml | www.grinninglizard.com/tinyxml |
Xerxes | xml.apache.crg/xerces-c |
Xalan | xml.apache.org/xalan-c |
Pathan 1 | software.decisionsoft.com/pathanIntro.html |
Boost.Serialization | www.boost.org/libs/serialization |
Табл. 14.2. Назначение библиотек
Имя библиотеки | Назначение | Рецепты |
---|---|---|
TinyXml | DOM (нестандартная версия) | Рецепт 14.1 |
Xerxes | SAX2, DOM, XML Schema | Рецепты 14.2-14.8 |
Xalan | XSLT, XPath | Рецепты 14.7-14.8 |
Pathan | XPath | Рецепт 14.8 |
Boost.Serialization | Сериализация XML | Рецепт 14.9 |
Имеется некоторая совокупность данных, хранимых в документе XML. Требуется выполнить синтаксический анализ документа и превратить эти данные в объекты C++. Документ XML имеет достаточно небольшой размер и может поместиться в оперативной памяти, причем в документе не используется внутреннее определение типа документа (Document Type Definition — DTD) и отсутствуют пространства имен XML.
Используйте библиотеку
TinyXml
. Во-первых, определите объект типа TiXmlDocument
и вызовите его метод LoadFile()
, передавая полное имя файла вашего XML-документа в качестве его аргумента. Если LoadFile()
возвращает значение «true», то это означает, что анализ вашего документа завершился успешно. В этом случае вызовите метод RootElement()
для получения указателя на объект типа TiXmlElement
, представляющего корневой элемент документа. Этот объект имеет иерархическую структуру, которая соответствует структуре вашего документа XML; выполняя проход по этой структуре, вы можете извлечь информацию о документе и использовать ее для создания набора объектов С++.
Например, предположим, что у вас имеется XML-документ animals.xml, описывающий некоторое количество животных цирка, как показано в примере 14.1. Корень документа имеет имя
animalList
и содержит несколько дочерних элементов animal
, каждый из которых представляет одно животное, принадлежащее цирку Feldman Family Circus. Предположим также, что у вас имеется класс C++ с именем Animal
, и вам нужно сконструировать вектор std::vector
, состоящий из объектов Animal
, представляющих животных, перечисленных в документе.
Пример 14.1. Документ XML со списком животных цирка
Herby
elephant
1992-04-23
Sheldon
parrot
1998-09-30
Dippy
penguin
2001-06-08
Пример 14.2 показывает, как может выглядеть определение класса
Animal
. Animal
имеет пять данных-членов, соответствующих кличке, виду, дате рождения, ветеринару и дрессировщику животного. Кличка и вид животного представляются строками типа std::string, дата его рождения представляется типом boost::gregorian::date
из Boost.Date_Time, а его ветеринар и дрессировщик представляются экземплярами класса Contact
, который определен также в примере 14.2. Пример 14.3 показывает, как можно использовать TinyXml
для синтаксического анализа документа animals.xml, просмотра разобранного документа и заполнения вектора std::vector
объектов Animal
, используя извлеченные из документа данные.
Пример 14.2. Заголовочный файл animal.hpp
#ifndef ANIMALS_HPP_INCLUDED
#define ANIMALS_HPP_INCLUDED
#include
#include
#include // runtime_error
#include
#include
// Представляет ветеринара или дрессировщика
class Contact {
public:
Contact() {}
Contact(const std::string& name, const std::string& phone) :
name_(name) {
setPhone(phone);
}
std::string name() const { return name_; }
std::string phone() const { return phone_; }
void setName(const std::string& name) { name_ = name; }
void setPhone(const std::string& phone) {
using namespace std;
using namespace boost;
// Используйте Boost.Regex, чтобы убедиться, что телефон
// задач в форме (ddd)ddd-dddd
static regex pattern("\\([0-9]{3}\\)[0-9]{3}-[0-9]{4}");
if (!regex_match(phone, pattern)) {
throw runtime_error(string("bad phone number:") + phone);
}
phone_ = phone;
}
private:
std::string name_;
std::string phone_;
};
// Сравнить на равенство два объекта класса Contact; используется в рецепте
// 14.9 (для полноты следует также определить operator!=)
bool operator--(const Contact& lhs, const Contact& rhs) {
return lhs.name() == rhs.name() && lhs.phone() == rhs.phone();
}
// Записывает объект класса Contact в поток ostream
std::ostream& operator(std::ostream& out, const Contact& contact) {
out << contact.name() << " " << contact.phone(); return out;
}
// Класс Animal представляет животное
class Animal {
public:
// Конструктор по умолчанию класса Animal; этот конструктор будет вами
// использоваться чаще всего Animal() {}
// Конструирование объекта Animal с указанием свойств животного;
// этот конструктор будет использован в рецепте 14.9
Animal(const std::string& name,
const std::string& species, const std::string& dob,
const Contact& vet, const Contact& trainer) :
name_(name), species_(species), vet_(vet), trainer_(trainer) {
setDateOfBirth(dob)
}
// Функции доступа к свойствам животного
std::string name() const { return name_; }
std::string species() const { return species_; }
boost::gregorian::date dateOfBirth() const { return dob_; )
Contact veterinarian() const { return vet_; }
Contact trainer() const { return trainer_; }
// Функции задания свойств животного
void setName(const std::string& name) { name_ = name; }
void setSpecies(const std::string& species) { species_ = species; }
void setDateOfBirth(const std::string& dob) {
dob_ = boost::gregorian::from_string(dob);
}
void setVeterinarian(const Contact& vet) { vet_ = vet; }
void setTrainer(const Contact& trainer) { trainer_ = trainer; }
private:
std::string name_;
std::string species_;
boost::gregorian::date dob_;
Contact vet_;
Contact trainer_;
};
// Сравнение на равенство двух объектов Animal; используется в рецепте 14.9
// (для полноты следует также определить operator!=)
bool operator==(const Animal& lhs, const Animal& rhs) {
return lhs.name() == rhs.name() && lhs.species() == rhs.species() &&
lhs.dateOfBirth() == rhs.dateOfBirth() &&
lhs.veterinarian() == rhs.veterinarian() &&
lhs.trainer() == rhs.trainer();
}
// Записывает объект Animal в поток ostream
std::ostream& operator<<(std::ostream& out, const Animal& animal) {
out << "Animal {\n"
<< " name=" << animal.name() << ";\n"
<< " species=" << animal.species() << ";\n"
<< date-of-birth=" << animal.dateOfBirth() << ";\n"
<< " veterinarian=" << animal.veterinarian() << ";\n"
<< " trainer=" << animal.trainer() << ";\n"
<< "}";
return out;
}
#endif // #ifndef ANIMALS_HPP_INCLUDED
Пример 14.3. Синтаксический анализ animals.xml с помощью TinyXml
#include
#include // cout
#include // runtime_error
#include // EXIT_FAILURE
#include // strcmp
#include
#include
#include "animal.hpp"
using namespace std;
// Извлекает текстовое содержимое элемента XML
const char* textValue("TiXmlElement* e) {
TiXmlNode* first = fi->FirstChild();
if (first != 0 && first == e->LastChild() &&
first->Type() == TiXmlNode::TEXT) {
// элемент «е» имеет один дочерний элемент типа TEXT;
// возвратить дочерний элемент
return first->Value();
} else {
throw runtime_error(string("bad ") + e->Value() + " element");
}
}
// Конструирует объект класса Contact из элементов ветеринара или
// дрессировщика ("veterinarian" или "trainer")
Contact nodeToContact(TiXmlElement* contact) {
using namespace std;
const char *name, *phone;
if (contact->FirstChild() == 0 &&
(name = contact->Attribute("name")) &&
(phone = contact->Attribute("phone"))) {
// Элемент contact не имеет дочерних элементов и имеет атрибуты имени
// и телефона ("name" и "phone"); используйте эти значения для
// конструирования объекта Contact
return Contact(name, phone);
} else {
throw runtime_error(string("bad ") + contact->Value() + " element");
}
}
// Конструирует объект Animal из элемента животного ("animal")
Animal nodeToAnimal(TiXmlElement* animal) {
using namespace std;
// Убедиться, что animal соответствует элементу "animal"
if (strcmp(animal->Value(), "animal") != 0) {
throw runtime_error(string("bad animal: ") + animal->Value());
}
Animal result; // Возвратить значение
TiXmlElement* element = animal->FirstChildElement();
// Прочитать элемент клички животного
if (element && strcmp(element->Value(), "name") == 0) {
// Первым дочерним элементом объекта animal является кличка (элемент
// name"); используйте ее текстовое значение для установки клички
// в объекте result
result.setName(textValue(element));
} else {
throw runtime_error("no name attribute");
}
// Прочитать элемент вида животного
element = element->NextSiblingElement();
if (element && strcmp(element->Value(), species") == 0) {
// Вторым дочерним элементом animal является вид животного
// (элемент "species"); используйте его текстовое значение для
// установки вида в объекте result
result.setSpecies(textValue(element));
} else {
throw runtime_error(""no species attribute");
}
// Прочитать элемент даты рождения
element = element->NextSiblingElement();
if (element && strcmp(element->Value(), "dateOfBirth") == 0) {
// Третьим дочерним элементом animal является дата рождения
// (элемент "dateOfBirth"));
// используйте его текстовое значение для установки даты
// рождения в объекте result
result.setDateOfBirth(textValue(element));
} else {
throw runtime_error("no dateOfBirth attribute");
}
// Прочитать элемент ветеринара
element = element->NextSiblingElement();
if (strcmp(element->Value(), "veterinarian") == 0) {
// Четвертым дочерним элементом animal является ветеринар (элемент
// "veterinarian"); используйте его для конструирования объекта
// Contact и установки имени ветеринара в объекте result
result.setVeterinarian(nodeToContact(element));
} else {
throw runtime_error("no veterinarian attribute");
}
// Прочитать элемент дрессировщика
element = element->NextSiblingElement();
if (strcmp(element->Value(), "trainer") == 0) {
// Пятым элементом animal является дрессировщик (элемент "trainer");
// используйте его для конструирования объекта
// Contact и установки дрессировщика в объекте result
result.setTrainer(nodeToContact(element));
} else {
throw runtime_error("no trainer attribute");
}
// Убедиться в отсутствии других дочерних элементов
element = element->NextSiblingElement();
if (element != 0) {
throw runtime_error(
string("unexpected element:") + element->Value()
);
}
return result;
}
int main() {
using namespace std;
try {
vector animalList;
// Обработать "animals.xml"
TiXmlDocument doc("animals.xml");
if (!doc.LoadFile())
throw runtime_error("bad parse");
// Убедиться, что корневым является список животных
TiXmlElement* root = doc.RootElement();
if (strcmp(root->Value(), "animalList") != 0) {
throw runtime_error(string("bad root: ") + root->Value());
}
// Просмотреть все дочерние элементы корневого элемента, заполняя
// список животных
for (TiXmlElement* animal = root->FirstChildElement();
animal; animal = animal->NextSiblingElement()) {
animalList.push_back(nodeToAnimal(animal));
}
// Напечатать клички животных
for (vector::size_type i = 0, n = animalList.size(); i < n; ++i) {
cout << animalList[i] << "\n";
}
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
TinyXml (буквально «крошечный XML») очень хорошо подходит в тех случаях, когда требуется выполнять несложную обработку документов XML. Дистрибутив исходных текстов этой библиотеки небольшой, ее легко построить и интегрировать в проекты, и она имеет очень простой интерфейс. Она также имеет очень либеральную лицензию. Главными ограничениями TinyXml являются невосприимчивость к пространствам имен XML, невозможность контроля DTD или схемы, а также невозможность анализа документов XML с внутренним DTD. Если вам требуется какая-то из этих функций или какая-нибудь XML-технология, как, например, XPath или XSLT, то необходимо воспользоваться другими библиотеками, рассмотренными в данной главе.
На выходе парсера TinyXml получается документ XML в виде дерева, узлы которого представляют элементы, текст, комментарии и другие компоненты документа XML. Корень дерева представляет собственно документ XML. Такое иерархическое представление документа называется объектной моделью документа (Document Object Model - DOM). Модель DOM, полученная парсером TinyXml, аналогична модели, разработанной консорциумом W3C (World Wide Web Consortium), хотя она и не полностью соответствует спецификации W3C. Вследствие приверженности библиотеки TinyXml принципам минимализма модель TinyXml DOM проще W3С DOM, однако она обладает меньшими возможностями.
Получить доступ к узлам дерева, представляющего документ XML, можно с помощью интерфейса
TiXmlNode
, который содержит методы, обеспечивающие доступ к родительскому узлу, последовательный доступ ко всем дочерним узлам, удаление и добавление дочерних узлов. Каждый узел является экземпляром некоторого производного типа; например, корень дерева является экземпляром TiXmlDocument
, узлы элементов являются экземплярами TiXmlElement
, а узлы, представляющие текст, являются экземплярами TiXmlText
. Тип TiXmlNode
можно определить с помощью его метода Туре()
; зная тип узла, вы можете получить конкретное его представление с помощью таких методов, как toDocument()
, toElement()
и toText()
. Эти производные типы содержат дополнительные методы, характерные для узлов конкретного типа.
Теперь несложно разобраться с примером 14.3. Во-первых, функция
textValue()
извлекает текстовое содержимое из элементов, содержащих только текст, например name
, species
или dateOfBirth
. В этом случае данная функция сначала убеждается, что имеется только один дочерний элемент и что он является текстовым узлом. Она затем получает текст дочернего элемента, вызывая метод Value()
, который возвращает текстовое содержимое текстового узла или узла комментария, имя тега узла элемента и имя файла корневого узла.
На следующем шаге функция
nodeToContact()
получает узел, соответствующий элементу veterinarian
или trainer
, и конструирует объект Contact
из значений атрибутов name
и phone
, получаемых с помощью метода Attribute()
.
Аналогично функция
nodeToAnimal()
получает узел, соответствующий элементу животного element, и конструирует объект Animal
. Это делается путем прохода по дочерним узлам с помощью метода NextSiblingElement()
, извлекая при этом содержащиеся в каждом элементе данные и устанавливая соответствующее свойство объекта Animal
. Данные извлекаются, используя функцию textValue()
для элементов name
, species
и dateOfBirth
и функцию nodeToContact()
для элементов veterinarian
и trainer
.
В функции
main
я сначала конструирую объект TiXmlDocument
соответствующий файлу animals.xml, и выполняю его синтаксический разбор с помощью метода LoadFile()
. Затем я получаю элемент TiXmlElement
, соответствующий корню документа, вызывая метод RootElement()
. На следующем шаге я просматриваю все дочерние узлы корневого элемента, конструируя объект Animal
из каждого элемента animal
с помощью функции nodeToAnimal()
. Наконец, я прохожу по всем объектам Animal
, записывая их в стандартный вывод.
В примере 14.3 не проиллюстрирована одна функция библиотеки
TinyXml
, а именно метод SaveFile()
класса TiXmlDocument
, который записывает в файл документ, представляемый объектом TiXmlDocument
. Это позволяет выполнить синтаксический разбор документа XML, модифицировать его, используя интерфейс DOM, и сохранить модифицированный документ. Документ TiXmlDocument
можно создать даже с чистого листа и затем сохранить его на диске.
// Создать документ hello.xml, состоящий
// из единственного элемента "hello"
TiXmlDocument doc;
TiXmlElement root("hello");
doc.InsertEndChild(root);
doc.SaveFile("hello.xml");
Рецепты 14.3 и 14.4.
Требуется обеспечить надежную и простую работу со строками с расширенным набором символов, используемыми библиотекой Xerces. В частности, необходимо уметь сохранять строки, возвращаемые функциями библиотеки Xerces, а также выполнять преобразования между строками Xerces и строками стандартной библиотеки С++.
Сохранять строки с расширенным набором символов, возвращаемые функциями библиотеки Xerces, можно с помощью шаблона
std::basic_string
, специализированного типом с расширенным набором символов XMLCh
библиотеки Xerces.
typedef std::basic_string XercesString;
Для выполнения преобразований между строками Xerces и строками, состоящими из стандартных символов, используйте перегруженный статический метод
transcode()
из класса xercesc::XMLString
, который определен в заголовочном файле xercesc/util/XMLString.hpp.
В примере 14.4 определяются две перегруженные вспомогательные функции,
toNative
и fromNative
, которые используют transcode
для преобразования строк со стандартными символами в строки Xerces
и обратно. Каждая функция имеет две версии: одна принимает строку в C-стиле, а другая принимает строку стандартной библиотеки С++. Для выполнения преобразований между строками Xerces и строками со стандартными символами вполне достаточно иметь эти служебные функции; после того как вы определили эти функции, вам уже никогда не потребуется вызывать непосредственно transcode
.
Пример 14.4. Заголовочный файл xerces_strings.hpp, используемый для выполнения преобразований между строками Xerces и строками со стандартными символами
#ifndef XERCES_STRINGS_HPP_INCLUDED
#define XERCES_STRINGS_HPP_INCLUDED
#include
#include
#include
typedef std::basic_string XercesString;
// Преобразует строку со стандартными символами
// в строку с расширенным набором символов
inline XercesString fromNative(const char* str) {
boost::scoped_array ptr(xercesc::XMLString::transcode(str));
return XercesString(ptr.get());
}
// Преобразует строку со стандартными символами
// в строку с расширенным набором символов.
inline XercesString fromNative(const std::string& str) {
return fromNative(str.c_str());
}
// Преобразует строку с расширенным набором символов
// в строку со стандартными символами.
inline std::string toNative(const XMLCh* str) {
boost::scoped_array ptr(xercesc::XMLString::transcode(str));
return std::string(ptr.get());
}
// Преобразует строку с расширенным набором символов в строку со стандартными символами.
inline std::string toNative(const XercesString& str) {
return toNative(str.c_str());
}
#endif // #ifndef XERCES_STRINGS_HPP_INCLUDED
Для выполнения преобразований между строками Xerces и
std::wstring
просто используйте конструктор std::basic_string
, передавая ему два итератора. Например, можно определить следующие две функции.
// Преобразует строку Xerces в строку std::wstring
std::wstring xercesToWstring(const XercesString& str) {
return std::wstring(str.begin(), str.end());
}
// Преобразует строку std::wstring в строку XercesString
XercesString wstringToXerces(const std::wstring& str) {
return XercesString(str.begin(), str.end());
}
В этих функциях используется тот факт, что
wchar_t
и XMLCh
являются интегральными типами, каждый из которых может неявно преобразовываться в другой; это должно работать независимо от размера wchar_t
, пока не используются значения, выходящие за диапазон XMLCh
. Вы можете определить подобные функции, принимающие в качестве аргументов строки в C-стиле, используя конструктор std::basic::string
, которому передаются в качестве аргументов массив символов и длина.
Для представления строк в коде Unicode библиотека Xerces использует последовательности символов
XMLCh
, завершаемые нулем. Тип XMLCh
вводится с помощью typedef
как интегральный тип, зависящий от реализации и содержащий не менее 16 бит, которых достаточно для представления символов почти любого языка. Xerces применяет символьную кодировку UTF-16, что подразумевает теоретическую возможность представления некоторых символов в коде Unicode в виде последовательности из нескольких символов XMLCh
; однако практически можно считать, что каждый символ XMLCh
непосредственно представляет один символ в коде Unicode, т.е. имеет числовое значение символа Unicode.
Одно время тип
XMLCh
определялся с помощью typedef
как wchar_t
, что позволяло легко сохранять копию строки Xerces как std::wstring
. Однако в настоящее время Xerces определяет XMLCh
на всех платформах с помощью typedef
как unsigned short
. Кроме всего прочего это означает, что на некоторых платформах типы XMLCh
и wchar_t
имеют разный размер. Поскольку Xerces может изменить в будущем определение XMLCh
, нельзя рассчитывать на то, что XMLCh
будет идентичен какому-то конкретному типу. Поэтому, если требуется сохранить копию строки Xerces, следует использовать тип std::basic_string
.
При использовании Xerces вам придется часто выполнять преобразования между строками со стандартными символами и строками Xerces; для этой цели в Xerces предусмотрена перегруженная функция
transcode()
. transcode()
может преобразовать строку Unicode в строку со стандартными символами, использующую «родную» кодировку символов, или строку с «родной» кодировкой со стандартными символами в строку Unicode. Однако смысл родной кодировки точно не определен, поэтому если вы программируете в среде, в которой часто используется несколько кодировок символов, то вам придется все взять в свои руки и выполнять преобразования особым образом, используя либо фасет std::codecvt
, либо подключаемые службы перекодировки (pluggable transcoding services) библиотеки Xerces, описанные в документации Xerces. Однако во многих случаях вполне достаточно использовать transcode()
.
Память под возвращаемые функцией
transcode()
строки, завершающиеся нулем, динамически выделяется при помощи оператора new
в форме массива; вам придется строку удалять самому, используя оператор delete[]
. Это создает небольшую проблему управления памяти, поскольку обычно требуется копировать строку или записывать ее в поток до ее удаления, а эти операции могут выбросить исключение. Я решаю эту проблему в примере 14.4 с помощью шаблона boost::scoped_array
, который динамически выделяет память под массив и автоматически удаляет его при выходе из области видимости, даже если выбрасывается исключение. Например, рассмотрим реализацию функции fromNative
.
inline XercesString fromNative(const char* str) {
boost::scoped_array ptr(xercesc::XMLString::transcode(str));
return XercesString(ptr.get());
}
Здесь
ptr
становится обладателем возвращенной функцией transcode()
строки с нулевым завершающим символом и освобождает ее, даже если конструктор XercesString
выбрасывает исключение std::bad_alloc
.
Имеется некоторый набор данных, хранимых в документе XML, внутри которого используется DTD или применяются пространства имен XML. Требуется выполнить синтаксический анализ документа и превратить содержащиеся в нем данные в набор объектов C++.
Используйте реализацию Xerces в виде программного интерфейса SAX2 (простой программный интерфейс для XML, версия 2.0). Во-первых, создайте класс, производный от
xercesc::ContentHandler
; этот класс будет получать уведомления с информацией о структуре и содержимом вашего документа XML по мере его анализа. Затем при желании можно создать класс, производный от xercesc::ErrorHandler
, для получения предупреждений и сообщений об ошибках. Сконструируйте парсер типа xercesc::SAX2XMLReader
, зарегистрируйте экземпляры классов вашего обработчика, используя методы парсера setContentHandler()
и setErrorHandler()
. Наконец, вызовите метод парсера parse()
, передавая в качестве аргумента полное имя файла, в котором содержится ваш документ.
Например, пусть требуется выполнить синтаксический анализ документа XML animals.xml, приведенного в примере 14.1, и сконструировать вектор
std::vector
объектов Animal
, представляющих животных, перечисленных в этом документе. (Определение класса Animal
дается в примере 14.2.) В примере 14.3 я показываю, как можно это сделать, используя TinyXml. Для усложнения задачи добавим в документ пространства имен, как показано в примере 14.5.
Пример 14.5. Список цирковых животных, в котором используются пространства имен XML
Herby
elephant
1992-04-23
Для анализа этого документа с помощью SAX2 определите
ContentHandler
, как показано в примере 14.6, и ErrorHandler
, как показано в примере 14.7. Затем сконструируйте SAX2XMLReader
, зарегистрируйте ваши обработчики и запустите парсер. Это проиллюстрировано в примере 14.8.
Пример 14.6. Применение SAX2 ContentHandler для синтаксического анализа документа animals.xml
#include // runtime_error
#include
#include
#include // Содержит реализации без
// операций для различных
// обработчиков, используемых
#include "xerces_strings.hpp" // в примере 14.4
#include "animal.hpp"
using namespace std;
using namespace xercesc;
// Возвращает экземпляр Contact, построенный
// на основе заданного набора атрибутов
Contact contactFromAttributes(const Attributes &attrs) {
// Для повышения эффективности хранить часто используемые строки
// в статических переменных
static XercesString name = fromNative("name");
static XercesString phone = fromNative("phone");
Contact result; // Возвращаемый объект Contact.
const XMLCh* val; // Значение атрибута name или phone.
// Установить имя объекта Contact.
if ((val = attrs.getValue(name.c_str())) != 0) {
result.setName(toNative(val));
} else {
throw runtime_error("contact missing name attribute");
}
// Установить номер телефона для объекта Contact.
if ((val = attrs.getValue(phone.c_str())) != 0) {
result.setPhone(toNative(val));
} else {
throw runtime_error("contact missing phone attribute");
}
return result;
}
// Реализует обратные вызовы, которые получают символьные данные и
// уведомления о начале и конце элементов
class CircusContentHandler : public DefaultHandler {
public:
CircusContentHandler(vector& animalList) :
animalList_(animalList) {}
// Если текущий элемент представляет ветеринара или дрессировщика
// используйте attrs для конструирования объекта Contact для текущего
// Animal; в противном случае очистите currentText_, подготавливая
// обратный вызов characters()
void startElement(
const XMLCh *const uri, // URI пространства имен
const XMLCh *const localname, // имя тега без префикса NS
const XMLCh *const qname, // имя тега + префикс NS
const Attributes &attrs) // атрибуты элементов
{
static XercesString animalList = fromNative("animalList");
static XercesString animal = fromNative("animal");
static XercesString vet = fromNative("veterinarian");
static XercesString trainer = fromNative("trainer");
static XercesString xmlns =
fromNative("http://www.feldman-family-circus.com");
// проверить URI пространства имен
if (uri != xmlns)
throw runtime_error(
string("wrong namespace uri ") + toNative(uri)
);
if (localname == animal) {
// Добавить в список объект Animal; это будет
// "текущий объект Animal"
animalList_.push_back(Animal());
} else if (localname != animalList) {
Animal& animal = animalList_.back();
if (localname == vet) {
// Мы встретили элемент "ветеринар".
animal.setVeterinarian(contactFromAttributes(attrs));
} else if (localname == trainer) {
// Мы встретили элемент "дрессировщик".
animal.setTrainer(contactFromAttributes(attrs));
} else {
// Мы встретили элемент "кличка", "вид животного" или
// "дата рождения". Их содержимое будет получено
// при обратном вызове функции characters().
currentText_.clear();
}
}
}
// Если текущий элемент представляет кличку, вид животного или дату
// рождения, используйте хранимый в currentText_ текст для установки
// соответствующего свойства текущего объекта Animal.
void endElement(
const XMLCh *const uri, // URI пространства имен
const XMLCh *const localname, // имя тега без префикса NS
const XMLCh *const qname) // имя тега + префикс NS
{
static XercesString animalList = fromNative("animal-list");
static XercesString animal = fromNative("animal");
static XercesString name = fromNative("name");
static XercesString species = fromNative("species");
static XercesString dob = fromNative("dateOfBirth");
if (localname!= animal && localname!= animalList) {
// currentText_ содержит текст элемента, который был
// добавлен. Используйте его для установки свойств текущего
// объекта Animal.
Animal& animal = animalList_.back();
if (localname == name) {
animal setName(toNative(currentText_));
} else if (localname == species) {
animal.setSpecies(toNative(currentText_));
} else if (localname == dob) {
animal.setDateOfBirth(toNative(currentText_));
}
}
}
// Получает уведомления, когда встречаются символьные данные
void characters(const XMLCh* const chars,
const unsigned int length) {
// Добавить символы в конец currentText_ для обработки методом
// endElement()
currentText_.append(chars, length);
}
private:
vector& animalList_;
XercesString currentText_;
};
Пример 14.7. SAX2 ErrorHandler
#include // runtime_error
#include
// Получает уведомления об ошибках.
class CircusErrorHandler : public DefaultHandler {
public:
void warning(const SAXParseException& e) {
/* нет действий */
}
void error(const SAXParseExceptionf& e) {
throw runtime_error(toNative(e.getMessage()));
}
void fatalError(const SAXParseException& e) { error(e); }
};
Пример 14.8. Синтаксический анализ документа animals.xml при помощи программного интерфейса SAX2
#include
#include // cout
#include // auto_ptr
#include
#include
#include
#include
#include "animal.hpp"
#include "xerces_strings.hpp" // Пример 14.4
using namespace std;
using namespace xercesc;
// Утилита RAII инициализирует парсер и освобождает ресурсы
// при выходе из области видимости
class XercesInitializer {
public:
XercesInitializer() { XMLPlatformUtils::Initialize(); }
~XercesInitializer() { XMLPlatformUtils::Terminate(); }
private:
// Запретить копирование и присваивание
XercesInitializer(const XercesInitializer&);
XercesInitializer& operator=(const XercesInitializer&);
};
int main() {
try {
vector animalList;
// Инициализировать Xerces и получить парсер
XercesInitializer init;
auto_ptr
parser(XMLReaderFactory::createXMLReader());
// Зарегистрировать обработчики
CircusContentHandler content(animalList);
CircusErrorHandler error;
parser->setContentHandler(&content);
parser->setErrorHandler(&error);
// Выполнить синтаксический анализ документа XML
parser->parse("animals.xml");
// Напечатать клички животных
for (vector::size_type i = 0;
n = animalList.size(); i < n; ++i) {
cout << animalList[i] << "\n";
}
} catch (const SAXException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const XMLException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
Некоторые парсеры XML выполняют синтаксический анализ документа XML и возвращают его пользователю в виде сложного объекта С++. Именно это делает парсер TinyXml и парсер W3C DOM, который будет рассмотрен в следующем рецепте. В отличие от них парсер SAX2 использует ряд функций обратного вызова для передачи пользователю информации о документе XML по ходу его анализа. Функции обратного вызова сгруппированы в несколько интерфейсов обработчиков:
ContentHandler
получает уведомления об элементах, атрибутах и о тексте документа XML, ErrorHandler
получает предупреждения и сообщения об ошибках, a DTDHandler
получает уведомления о DTD документа XML.
Проектирование парсера, использующего функции обратного вызова, имеет несколько важных преимуществ. Например, можно выполнять синтаксический анализ очень больших документов, которые не помещаются в памяти. Кроме того, это может сэкономить процессорное время, потому что не надо выполнять многочисленные операции динамического выделения памяти, необходимые для конструирования узлов внутреннего представления документа XML, и потому что пользователь может создавать свое представление данных документа непосредственно, а не во время прохождения дерева документа, как я это делал в примере 14.3.
Пример 14.8 достаточно простой: я получаю парсер SAX2, регистрирую
ContentHandler
и ErrorHandler
, анализирую документ animals.xml
и печатаю список объектов Animal
, заполненный обработчиком ContentHandler
. Следует отметить два интересных момента: во-первых, функция XMLReaderFactory::createXMLReader()
возвращает экземпляр SAX2XMLReader
, память под который выделяется динамически и должна освобождаться пользователем в явной форме; для этой цели я использую std::auto_ptr
, чтобы обеспечить удаление парсера даже в случае возникновения исключения. Во-вторых, среда Xerces должна быть инициализирована, используя xercesc::XMLPlatformUtils::Initialize()
, и очищена при помощи xercesc::XMLPlatformUtils::Terminate()
. Я инкапсулирую эту инициализацию и очистку в классе XercesInitializer
, который вызывает XMLPlatformUtils::Initialize()
в своем конструкторе и XMLPlatformUtils::Terminate()
в своем деструкторе. Это гарантирует вызов Terminate()
, даже если выбрасывается исключение. Это пример метода захвата ресурса при инициализации (Resource Acquisition Is Initialization — RAII), который был продемонстрирован в примере 8.3.
Давайте теперь посмотрим, как класс
CircusContentHandler
из примера 14.6 реализует интерфейс SAX2 ContentHandler
. Парсер SAX 2 вызывает метод startElement()
при каждой встрече открывающего тега элемента. Если элементу приписано пространство имен, первый аргумент, uri
, будет содержать URI пространства имен элемента, а второй аргумент, localname
, будет содержать ту часть имени тега элемента, которая идет за префиксом пространства имен. Если элемент не имеет пространства имен, эти два аргумента будут иметь пустые строки. Третий аргумент содержит имя тега элемента, если с элементом не связывается пространство имен; в противном случае этот аргумент может содержать либо имя тега элемента в том виде, в каком оно встречается в анализируемом документе, либо пустую строку. Четвертым аргументом является экземпляр класса Attributes
, представляющего набор атрибутов элемента.
В приведенной в примере 14.6 реализации
startElement()
я игнорирую элемент animalList
. Когда я встречаю элемент animal
, я добавляю новый объект Animal
в список животных; назовем его текущим объектом Animal
и предоставим право установки свойств этого Animal
обработчикам других элементов. Когда я встречаю элемент veterinarian
или trainer
, я вызываю функцию contactFromAttributes
для конструирования экземпляра Contact
из набора атрибутов элемента и затем использую этот объект Contact
для установки свойств ветеринара и дрессировщика в текущем элементе Animal
. Когда я встречаю элемент name, species
или dateOfBirth
, я очищаю переменную-член currentText_
, которая будет использоваться для хранения текстового содержимого этого элемента.
Парсер SAX2 вызывает метод
characters()
для передачи символьных данных, содержащихся в элементе. Этот парсер может передавать символы элемента с помощью нескольких вызовов метода characters()
; пока не встретится закрывающий тег, нельзя быть уверенным в передаче всех символьных данных. Поэтому в реализации characters()
я просто добавляю полученные символы в конец переменной-члена currentText_
, которую я использую для установки клички, вида и даты рождения Animal
сразу после встречи закрывающего тега для элемента name
, species
или dateOfBirth
.
Парсер SAX2 вызывает метод
endElement()
при выходе из каждого элемента. Его аргументы имеют тот же смысл, который имеют первые три аргумента метода startElement()
. В реализации endElement()
, приведенной в примере 14.6, я игнорирую все элементы, отличные от name
, species
и dateOfBirth
. Когда происходит обратный вызов, соответствующий одному из этих элементов, сигнализирующий о сделанном только что выходе парсера из элемента, я использую символьные данные, сохраненные в currentText_
для установки клички, вида и даты рождения текущего объекта Animal
.
Несколько важных особенностей SAX2 не проиллюстрировано в примерах 14.6, 14.7 и 14.8. Например, класс
SAX2XMLReader
содержит перегрузку метода parse()
, которая принимает в качестве аргумента экземпляр xercesc::InputSource
вместо строки в С-стиле. InputSource
является абстрактным классом, инкапсулирующим источник символьных данных; конкретные его подклассы, в том числе xercesc::MemBufInputSource
и xercesc::URLInputSource
, позволяют парсеру SAX2 анализировать документ XML, который находится не в локальной файловой системе.
Более того, интерфейс
ContentHandler
содержит много дополнительных методов, например startDocument()
и endDocument()
, которые сигнализируют о начале и конце документа XML, и setLocator()
, который позволяет задать объект Locator
, отслеживающий текущую позицию анализируемого файла. Существуют также другие интерфейсы обработчиков, включая DTDHandler
и EntityResolver
(соответствующие базовой спецификации SAX 2.0), а также DeclarationHandler
и LexicalHandler
(соответствующие стандартизованным расширениям SAX 2.0).
Кроме того, можно в одном классе реализовать несколько интерфейсов обработчиков. Это можно легко сделать в классе
xercesc::DefaultHandler
, потому что он является производным от всех интерфейсов обработчиков и содержит реализации своих виртуальных функций, в которых не выполняется никаких действий. Следовательно, я мог бы добавить методы из CircusErrorHandler
в CircusContentHandler
и следующим образом модифицировать пример 14.8.
// Зарегистрировать обработчики
CircusContentHandler handler(animalList);
parser->setContentHandler(&handler);
parser->setErrorHandler(&handler);
Пример 14.8 имеет еще одну, последнюю особенность, которую вы должны были заметить: обработчик
CircusContentHandler
не проверяет корректность структуры экземпляра анализируемого документа, т.е. не убеждается в том, что корневым является элемент animalList
или что все дочерние элементы корня являются элементами animal
. Это сильно отличается от примера 14.3. Например, функция main()
из примера 14.3 проверяет то, что элементом верхнего уровня является animalList
, а функция nodeToAnimal()
проверяет то, что ее аргументы представляют элемент animal
, содержащий точно пять дочерних элементов типа name
, species
, dateOfBirth
, veterinarian
и trainer
.
Пример 14.6 можно модифицировать, чтобы он выполнял подобного рода проверки. Например, обработчик
ContentHandler
в примере 14.9 удостоверяется в том, что корневым элементом документа является animalList
и что его дочерние элементы имеют тип animal
, а дочерние элементы элемента animal
не содержат других элементов. Это можно сделать с помощью трех флагов типа boolean
, parsingAnimalList_
, parsingAnimal_
и parsingAnimalChild_
, которые регистрируют анализируемую в данный момент область документа. Методы startElement()
и endElement()
просто обновляют эти флаги и проверяют их согласованность, делегируя задачу обновления текущего объекта Animal вспомогательным методам startAnimalChild()
и endElementChild()
, реализация которых очень напоминает реализацию методов startElement()
и endElement()
из примера 14.6.
Пример 14.9. Обработчик SAX2 ContentHandler документа animals.xml, который проверяет структуру документа
// Реализует функции обратного вызова, которые получают символьные данные и
// уведомляют о начале и конце элементов
class CircusContentHandler : public DefaultHandler {
public:
CircusContentHandler(vector& animalList)
: animalList_(animalList), // заполняемый список
parsingAnimalList_(false), // состояние анализа
parsingAnimal_(false), // состояние анализа
parsingAnimalChild_(false) // состояние анализа
{}
// Получает уведомления от парсера при каждой встрече начала
// какого-нибудь элемента
void startElement(
const XMLCh *const uri, // uri пространства имен
const XMLCh *const localname, // простое имя тега
const XMLCh *const qname, // квалифицированное имя тега
const Attributes &attrs) // Набор атрибутов
{
static XercesString animalList = fromNative("animalList");
static XercesString animal = fromNative("animal");
static XercesString xmlns =
fromNative("http://www.feldman-family-circus.com");
// Проверяет uri пространства имен
if (uri != xmlns)
throw runtime_error(
string("wrong namespace uri: ") + toNative(uri)
);
// (i) Обновить флаги parsingAnimalList_, parsingAnimal_
// и parsingAnimalChild_, которые показывают, в какой части
// документа мы находимся
// (ii) Убедиться, что элементы имеют правильную вложенность
//
// (iii) Делегировать основную работу методу
// startAnimalChild()
if (!parsingAnimalList_) {
// Мы только что встретили корень документа
if (localname == animalList) {
parsingAnimalList_ = true; // Обновить состояние анализа.
} else {
// Неправильная вложенность
throw runtime_error(
string("expected 'animalList', got ") + toNative(localname)
);
}
} else if (!parsingAnimal_) {
// Мы только что встретили новое животное
if (localname == animal) {
parsingAnimal_ = true; // Обновить состояние
// анализа.
animalList_.push_back(Animal()); // Добавить в список объект
// Animal.
} else {
// Неправильная вложенность
throw runtime error(
string("expected 'animal', got ") + toNative(localname)
);
}
} else {
// Вы находимся в середине анализа элемента, описывающего
// животного.
if (parsingAnimalChild_) {
// Неправильная вложенность
throw runtime_error("bad animal element");
}
// Обновить состояние анализа
parsingAnimalChild_ = true;
// Пусть startAnimalChild() выполнит реальную работу
startAnimalChild(uri, localname, qname, attrs);
}
}
void endElement(
const XMLCh *const uri, // uri пространства имен
const XMLCh *const localname, // простое имя тега
const XMLCh *const qname ) // квалифицированное имя тега
{
static XercesString animalList = fromNative("animal-list");
static XercesString animal = fromNative("animal");
// Обновить флаги parsingAnimalList, parsingAnimal_
// и parsingAnimalChild_; делегировать основную работу
// endAnimalChild()
if (localname == animal) {
parsingAnimal_ = false;
} else if (localname == animalList) {
parsingAnimalList_ = false;
} else {
endAnimalChild(uri, localname, qname);
parsingAnimalChild_ = false;
}
}
// Получает уведомления о встрече символьных данных
void characters(const XMLCh* const chars, const unsigned int length) {
// Добавляет символы в конец currentText_ для обработки методом
// endAnimalChild()
currentText.append(chars, length);
}
private:
// Если текущий элемент представляет ветеринара или дрессировщика,
// используйте attrs для конструирования объекта Contact для
// текущего Animal; в противном случае очистите currentText_,
// подготавливая обратный вызов characters()
void startAnimalChild(
const XMLCh *const uri, // uri пространства имен
const XMLCh *const localname, // простое имя тега
const XMLCh *const qname, // квалифицированное имя тега
const Attributes &attrs ) // Набор атрибутов
{
static XercesString vet = fromNative("veterinarian");
static XercesString trainer = fromNative("trainer");
Animal& animal = animalList_.back();
if (localname == vet) {
// Мы встретили элемент "ветеринар".
animal.setVeterinarian(contactFromAttributes(attrs));
} else if (localname == trainer) {
// Мы встретили элемент "дрессировщик".
animal.setTrainer(contactFromAttributes(attrs));
} else {
// Мы встретили элемент "кличка , "вид" или
// "дата рождения". Его содержимое будет передано функцией
// обратного вызова characters().
currentText_.clear();
}
}
// Если текущий элемент представляет кличку, вид или дату рождения,
// используйте текст, находящийся в currentText_, для установки
// соответствующего свойства текущего объекта
Animal. void endAnimalChild(
const XMLCh *const uri, // uri пространства имен
const XMLCh *const localname, // простое имя тега
const XMLCh *const qname) // квалифицированное имя тега
{
static XercesString name = fromNative("name");
static XercesString species = fromNative("species");
static XercesString dob = fromNative("dateOfBirth");
// currentText_ содержит текст элемента, который только что
// закончился. Используйте его для установки свойств текущего
// объекта Animal.
Animal& animal = animalList_.back();
if (localname == name) {
animal.setName(toNative(currentText_));
} else if (localname == species) {
animal.setSpecies(toNative(currentText_));
} else if (localname == dob) {
animal.setDateOfBirth(toNative(currentText_));
}
}
vector& animalList_; // заполняемый список
bool parsingAnimalList_; // состояние анализа
bool parsingAnimal_; // состояние анализа
bool parsingAnimalChild_; // состояние анализа
XercesString currentText_; // символьные данные текущего
// текстового узла
};
Из сравнения примера 14.9 с примером 14.6 видно, насколько сложным может быть проверка структуры документа с помощью функций обратного вызова. Более того, в примере 14.6 не делается столько проверок, как в примере 14.3: здесь, например, не проверяется порядок следования дочерних элементов элемента животного. К счастью, существует гораздо более простой способ проверки структуры документа с использованием SАХ2, как вы это увидите в рецептах 14.5 и 14.6.
Рецепты 14.1, 14.4, 14.5 и 14.6.
Требуется представить документ XML в виде объекта С++, чтобы можно было манипулировать его элементами, атрибутами, текстом, DTD, инструкциями обработки и комментариями
Используйте реализованную в Xerces модель W3C DOM. Во-первых, используйте класс
xercesc::DOMImplementationRegistry
для получения экземпляра xercesc::DOMImplementation
, затем используйте DOMImplementation
для создания экземпляра парсера xercesc::DOMBuilder
. На следующем шаге зарегистрируйте экземпляр xercesc::DOMErrorHandler
для получения уведомлений об ошибках, обнаруженных в ходе анализа, и вызовите метод парсера parseURI()
, передавая в качестве аргумента URI документа XML или полное имя файла. Если анализ документа завершается успешно, метод parseURI
возвратит указатель на объект DOMDocument
, представляющий документ XML. Затем вы можете использовать функции, определенные в спецификации W3C DOM для просмотра и манипулирования документом.
Обработав документ, вы можете сохранить его в файле, получая
DOMWriter
из DOMImplementation
и вызывая его метод writeNode()
с передачей указателя на DOMDocument
в качестве аргумента.
Пример 14.10 показывает, как можно использовать DOM для синтаксического анализа документа animals.xml, приведенного в примере 14.1, затем найти и удалить узел, относящийся к слону Herby, и сохранить модифицированный документ.
Пример 14.10. Применение DOM для загрузки, модификации и затем сохранения документа XML
#include
#include // cout
#include
#include
#include
#include
#include "animal.hpp"
#include "xerces_strings.hpp"
using namespace std;
using namespace xercesc;
/*
* Определить XercesInitializer, как это сделано в примере 14.8
*/
// Утилита RAII, которая освобождает ресурс при выходе из области видимости.
template
class DOMPtr {
public:
DOMPtr(T* t) : t_(t) {}
~DOMPtr() { t_->release(); }
T* operator->() const { return t_; }
private:
// запретить копирование и присваивание
DOMPtr(const DOMPtr&);
DOMPtr& operator=(const DOMPtr&);
T* t_;
};
// Сообщает об ошибках, обнаруженных в ходе синтаксического анализа с
// использованием DOMBuilder.
class CircusErrorHandler : public DOMErrorHandler {
public:
bool handleFrror(const DOMError& e) {
std::cout << toNative(e.getMessage()) << "\n";
return false;
}
};
// Возвращает значение элемента "name", дочернего по отношению к элементу
// "animal".
const XMLCh* getAnimalName(const DOMElement* animal) {
static XercesString name = fromNative("name");
// Просмотреть дочерние элементы объекта animal
DOMNodeList* children = animal->getChildNodes();
for (size_t i = 0, len = children->getLength(); i < Len; ++i) {
DOMNode* child = children->item(i);
if (child->getNodeType() == DOMNode::ELEMENT_NODE &&
static_cast(child)->getTagName() == name) {
// Мы нашли элемент "name".
return child->getTextContent();
}
}
return 0;
}
int main() {
try {
// Инициализировать Xerces и получить DOMImplementation;
// указать, что требуется функция загрузки и сохранения (Load and
// Save - LS)
XercesInitializer init;
DOMImplementation* impl =
DOMImplementationRegistry::getDOMImplementation(fromNative("LS").c_str()
);
if (impl == 0) {
cout << "couldn't create DOM implementation\n";
return EXIT_FAILURE;
}
// Сконструировать DOMBuilder для анализа документа animals.xml.
DOMPtr parser =
static_cast(impl)->
createDOMBuilder(DOMImplementationLS::MODE_SYNCHRONOUS, 0);
// Подключить пространства имен (они не требуются в этом примере)
parser->setFeature(XMLUni::fgDOMNamespaces, true);
// Зарегистрировать обработчик ошибок
CircusErrorHandler err;
parser->setErrorHandler(&err);
// Выполнить синтаксический анализ animals.xml; здесь можно
// использовать URL вместо имени файла
DOMDocument* doc =
parser->parseURI("animals.xml");
// Найти элемент слона Herby: сначала получить указатель на элемент
// "animalList".
DOMElement* animalList = doc->getDocumentElement();
if (animalList->getTagName() != fromNative("animalList")) {
cout << "bad document root: "
<< toNative(animalist->getTagName()) << "\n";
return EXIT_FAILURE;
}
// Затем просматривать элементы "animal", пытаясь найти элемент слона
// Herby.
DOMNodeList* animals =
animaIList->getElementsByTagName(fromNative("animal").c_str());
for (size_t i = 0,
len = animals->getLength(); i < len; ++i) {
DOMElement* animal =
static_cast(animals->item(i));
const XMLCh* name = getAnimalName(animal);
if (name != 0 && name == fromNative("Herby")) {
// Herby найден - удалить его из документа.
animalList->removeChild(animal);
animal->release();
// необязательный оператор.
break;
}
}
// Сконструировать DOMWriter для сохранения animals.xml.
DOMPtr writer =
static cast(impl)->createDOMWriter();
writer->setErrorHandler(&err);
// Сохранить animals.xml.
LocalFileFormatTarget file("animals.xml");
writer->writeNode(&file, *animalList);
} catch (const SAXException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const DOMException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
Подобно парсеру TinyXml парсер Xerces DOM на выходе формирует документ XML в виде объекта C++, имеющего структуру дерева, узлы которого представляют компоненты документа. Однако парсер Xerces существенно сложнее: например, в отличие от TinyXml он «понимает» пространства имен XML и может выполнять синтаксический анализ сложных DTD. Этим парсером также формируется гораздо более детальное представление документа XML, включающее инструкции обработки и URI пространств имен, относящиеся к элементам и атрибутам. Очень важно то, что он предоставляет доступ к информации через интерфейс, описанный в спецификации W3C DOM.
Спецификация W3C, которая все еще дорабатывается, имеет несколько «уровней»; в настоящее время предусматривается три уровня. Классы
DOMImplementation
, DOMDocument
, DOMElement
и DOMNodeList
, использованные в примере 14.10, описываются на уровне 1 спецификации DOM. Классы DOMBuilder
и DOMWrite
описываются на уровне 3 спецификации DOM как часть рекомендаций по функции загрузки и сохранения (Load и Save).
Имена классов Xerces не всегда совладают с именами интерфейсов W3C DOM, которые они реализуют; это происходит из-за того, что Xerces реализует несколько спецификаций в одном пространстве имен и использует префиксы в именах классов, чтобы избежать конфликтов имен.
Понимание примера 14.10 теперь не должно вызывать затруднений. Я начинаю с инициализации Xerces, как это делается в примере 14.8. Затем я получаю
DOMImplementation
из DOMImplementationRegistry
, запрашивая функцию загрузки и сохранения путем передачи строки «LS
» статическому методу DOMImplementationRegistry::getDOMImplementation()
. На следующем шаге я получаю DOMBuilder
из DOMImplementation
. Мне приходится тип DOMImplementation
привести к типу DOMImplementationLS
, потому что функция загрузки и сохранения недоступна из интерфейса DOMImplementation
согласно спецификации W3C DOM уровня 1. Первый аргумент createDOMBuilder()
показывает, что возвращаемый парсер будет работать в синхронном режиме. Другой возможный режим, а именно асинхронный режим, в настоящее время не поддерживается в Xerces.
Получив
DOMBuilder
, я включаю поддержку пространства имен XML, регистрирую обработчик ErrorHandler
и выполняю синтаксический анализ документа. Парсер возвращает документ в виде DOMDocument
; используя метод getElementsByTagName()
документа DOMDocument
, я получаю объект DOMElement
, соответствующий элементу этого документа animalList
, и просматриваю его дочерние элементы, используя объект типа DOMNodeList
. Когда я нахожу элемент, имеющий дочерний элемент типа name
и содержащий текст «Herby
», я удаляю его из документа с помощью вызова метода корневого элемента removeChild()
.
Подобно тому как
SAX2XMLReader
имеет метод parse()
, принимающий экземпляр InputSource
, DOMBuilder
имеет метол parse()
, принимающий экземпляр xercesc::DOMInputSource
(т.е. экземпляр абстрактного класса, который инкапсулирует источник символьных данных). В DOMInputSource
предусмотрен конкретный подкласс Wrapper4DOMInputSource
, который может быть использован для преобразования произвольного InputSource
в xercesc::DOMInputSource
. См рецепт 14.3.
Наконец, я получаю объект
DOMWriter
из DOMImplementation
(причем делаю это во многом точно так же, как при получении объекта DOMBuilder
) и сохраняю модифицированный документ XML на диск, вызывая его метод writeNode()
, передавая в качестве аргумента корневой элемент документа.
Вы должны освободить указатели, возвращаемые методами с именами вида
DOMImplementation::createXXX()
, путем вызова метода release()
. Используйте утилиту DOMPtr
из примера 14.10 для того, чтобы гарантировать освобождение такого указателя, даже если выбрасывается исключение. Необязательно явно удалять указатели, возвращаемые методами, имена которых имеют вид DOMDocument::createXXX()
, хотя это можно делать, если они больше не нужны. Дополнительные сведения вы найдете в документации Xerces.
Требуется проверить документ XML на соответствие DTD.
Используйте библиотеку Xerces с парсером SAX2 (простой программный XML-интерфейс) или с парсером DOM.
Для проверки документа XML при использовании SAX2 получите
SAX2XMLReader
, как показано в примере 14.8. Затем включите режим проверки DTD, вызывая метод парсера setFeature()
с аргументами xercesc::XMLUni::fgSAX2CoreValidation
и true
. Наконец, зарегистрируйте обработчик ErrorHandler
для получения уведомлений о нарушении DTD и вызовите метод парсера parse()
, указывая в качестве его аргумента имя вашего документа XML.
Для проверки документа XML при использовании парсера DOM сначала сконструируйте экземпляр
XercesDOMParser
. Затем включите режим проверки DTD, вызывая метод парсера setValidationScheme()
с аргументом xercesc::XercesDOMParser::Val_Always
. Наконец, зарегистрируйте обработчик ErrorHandler
для получения уведомлений о нарушении DTD и вызовите метод парсера parse()
, указывая в качестве его аргумента имя вашего документа XML.
Здесь я использую класс
XercesDOMParser
, т.е. XML-парсер, который входил в состав Xerces еще до того, как был разработан интерфейс DOMBuilder
— парсера DOM уровня 3. Применение XercesDOMParser
позволяет немного упростить пример, но при желании вы можете вместо него использовать DOMBuilder
. См. обсуждение этого рецепта и рецепт 14.4.
Для примера предположим, что вы модифицируете документ XML animals.xml из примера 14.1 для того, чтобы он содержал ссылку на внешнее определение DTD, как показано в примерах 14.11 и 14.12. Программный код, выполняющий проверку документа с использованием программного интерфейса SAX2, приводится в примере 14.13; программный код, выполняющий проверку этого документа с использованием парсера DOM, приводится в примере 14.14.
Пример 14.11. DTD animals.dtd для файла animals.xml
veterinarian, trainer) >
name CDATA #REQUIRED
phone CDATA #REQUIRED
>
name CDATA #REQUIRED
phone CDATA #REQUIRED
>
Пример 14.12. Модифицированный файл animals.xml, содержащий DTD
Пример 14.13. Проверка документа animals.xml на соответствие DTD с использованием программного интерфейса SAX2
/*
* Операторы #include из примера 14.8, кроме включения вектора который
здесь не нужен
*/
#include // runtime_error
#include
using namespace std;
using namespace xercesc;
/*
* Определить XercesInitializer, как это сделано в примере 14.8, и
* CircusErrorHandler, как это сделано в примере 14.7
*/
int main() {
try {
// Инициализировать Xerces и получить парсер
SAX2 XercesInitializer init;
auto_ptr
parser(XMLReaderFactory::createXMLReader());
// Включить режим проверки
parser->setFeature(XMLUni::fgSAX2CoreValidation, true);
// Зарегистрировать обработчик ошибок для получения уведомлений о
// нарушениях DTD
CircusErrorHandler error;
parser->setErrorHandler(&error);
parser->parse("animals.xml");
} catch (const SAXException& e) {
cout << "xml error " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const XMLException& e) {
cout << "xml error " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
Пример 14.14. Проверка документа animals.xml на соответствие DTD animals.dtd с использованием парсера XercesDOMParser
#include
#include // cout
#include // runtime_error
#include
#include
#include
#include
#include "xerces_strings.hpp" // Пример 14.4
using namespace std;
using namespace xercesc;
/*
* Определить XercesInitializer, как это сделано в примере 14.8
* и CircusErrorHandler, как это сделано в примере 14.7
*/
int main() {
try {
// Инициализировать Xerces и сконструировать DOM-парсер.
XercesInitializer init;
XercesDOMParser parser;
// Включить режим проверки DTD
parser.setValidationScheme(XercesDOMParser::Val_Always);
// Зарегистрировать обработчик ошибок для получения уведомлений о
// нарушениях схемы
CircusErrorHandler handler;
parser.setErrorHandler(&handler);
// Выполнить синтаксический анализ вместе с проверкой.
parser.parse("animals.xml");
} catch (const SAXException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const XMLException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
Определения DTD обеспечивают простой способ наложения ограничений на документ XML. Например, в DTD можно указать, какие элементы допускаются в документе, какие атрибуты может иметь элемент и может ли конкретный элемент содержать дочерние элементы, текст или и то и другое. Можно также накладывать ограничения на тип, порядок следования и количество дочерних элементов, а также на значения атрибутов.
DTD предназначены для определения подмножества правильно сформированных документов XML, которые характерны для определенной прикладной области. Например, в примере 14.1 важно то, что каждый элемент
animal
имеет дочерние элементы name
, species
, dateOfBirth
, veterinarian
и trainer
, а элементы name
, species
и dateOfBirth
содержат только текст в то время, как элементы veterinarian
и trainer
имеют атрибуты name
и phone
. Более того, элемент animal
не должен иметь атрибут phone
, а элемент veterinarian
не должен иметь дочерний элемент species
.
DTD в примере 14.11 накладывает ограничения различного типа. Например, приведенное ниже объявление элемента устанавливает необходимость наличия в элементе животного дочерних элементов
name
, species
, dateOfBirth
, veterinarian
и trainer
, задаваемых именно в этом порядке.
veterinarian, trainer) >
Аналогично приведенное ниже объявление атрибута указывает на то, что элемент
trainer
должен иметь атрибуты name
и phone
, а отсутствие в DTD объявлений других атрибутов для элемента дрессировщика говорит о том, что этот элемент может иметь только два атрибута.
name CDATA #REQUIRED
phone CDATA #REQUIRED
>
Документ XML, который содержит DTD и удовлетворяет его требованиям, называют достоверным (valid). XML-парсер, который обнаруживает не только синтаксические ошибки, но и проверяет достоверность документа XML. называется подтверждающим парсером (validating parser). Хотя парсеры
SAX2XMLReader
и XercesDOMParser
не являются по умолчанию подтверждающими парсерами, в каждом из них предусмотрена функция подтверждения достоверности, которая может подключаться так, как это сделано в примерах 14.13 и 14.14. Аналогично парсер DOMBuilder
, описанный в рецепте 14 4, может проверять достоверность документа XML, вызывая свой метод setFeaturе()
с аргументами fgXMLUni::fgDOMValidation
и true
.
Классы
SAX2XMLReader
, DOMBuilder
, DOMWriter
и XercesDOMParser
поддерживают ряд дополнительных функций. В SAX2XMLReader
и DOMBuilder
вы можете включать эти функции, используя методы setFeature()
и setProperty()
. Первый метод принимает строку и булево значение: второй метод принимает строку и void*
. Запросить включенные функции можно с помощью методов getFeature()
и getProperty()
. Для удобства в Xerces предусмотрены константы с именами фикций и свойств. Класс DOMWriter
поддерживает setFeature()
, но не поддерживает setProperty()
. Класс XercesDOMParser
поддерживает оба метода, в нем предусмотрены отдельные методы по установке и получению каждой функции. В документации Xerces вы найдете полный список поддерживаемых дополнительных функций.
Рецепт 14.6.
Требуется подтвердить соответствие документа XML схеме, представленной в рекомендациях XML Schema 1.0.
Используйте библиотеку Xerces совместно с программным интерфейсом SAX2 или с парсером DOM.
Подтверждение соответствия документа XML схеме с использованием программного интерфейса SAX2 осуществляется точно так же, как подтверждение достоверности документа, содержащего DTD, когда схема содержится внутри целевого документа или когда на нее делается ссылка в этом документе. Если требуется проверить документ XML на соответствие внешней схеме, вы должны вызвать метод парсера
setProperty()
для включения режима подтверждения внешней схемы. В качестве первого аргумента setProperty()
необходимо использовать XMLUni::fgXercesSchemaExternalSchemaLocation
или XMLUni::fgXercesSchemaExternalNoNameSpaceSchemaLocation
в зависимости оттого, используется или нет в схеме целевое пространство имен. Второй аргумент должен определять место расположения схемы, представленное значением типа const XMLCh*
. Не забудьте привести тип второго аргумента к void*
, как это сделано в рецепте 14.5.
Подтверждение соответствия документа XML схеме на основе использования
XercesDOMParser
выполняется аналогично подтверждению достоверности документа DTD, когда схема содержится внутри целевого документа или когда на нее делается ссылка в этом документе. Единственное отличие заключается в явном подключении средств поддержки схемы и пространства имен, как показано в примере 14.15.
Пример 14.15. Включение режима подтверждения схемы при использовании XercesDOMParser
XercesDOMParser parser;
parser.setValidationScheme(XercesDOMParser::Val_Always);
parser.setDoSchema(true);
parser setDoNamespaces(true);
Если требуется проверить документ XML на соответствие внешней схеме, имеющей целевое пространство имен, вызовите метод парсера
setExternalSchemaLocation()
, передавая в качестве аргумента место расположения вашей схемы. Если требуется проверить документ XML на соответствие внешней схеме, не имеющей целевого пространства имен, вызовите метод парсера setExternalNoNamespaceSchemaLocation()
.
Аналогично для проверки документа XML на соответствие схемы с использованием
DOMBuilder
включите функцию подтверждения достоверности следующим образом.
DOMBuilder* parser = ...;
parser->setFeature(XMLUni::fgDOMNamespaces, true);
parser->setFeature(XMLUni::fgDOMValidation, true);
parser->setFeature(XMLUni::fgXercesSchema, true);
Для подтверждения соответствия документа внешней схеме с использованием
DOMBuilder
установите свойство XMLUni::fgXercesSchemaExternalSchemaLocation
или XMLUni::fgXercesSchemaExternalNoNameSpaceSchemaLocation
в значение места расположения схемы.
Например, пусть требуется проверить документ animals.xml из примера 14.1, используя схему из примера 14.16. Один из способов заключается в добавлении ссылки на схему в документ animals.xml, как показано в примере 14.17. После этого вы можете проверить документ, используя программный интерфейс SAX2, как показано в примере 14.13, или используя DOM, как показано в примере 14.14 с учетом модификаций, выполненных в примере 14.15.
Пример 14.16. Схема animals.xsd для файла animals.xml
elementFormDefault="qualified">
Пример 14.17. Модифицированный файл animals.xml, содержащий ссылку на схему
xsi:noNamespaceSchemalocation="animals.xsd">
Можно поступить по-другому: опустить ссылку на схему и включить режим подтверждения соответствия документа внешней схеме. Пример 14.18 показывает, как это можно сделать при использовании парсера DOM.
Пример 14.18. Подтверждение соответствия документа XML внешней схеме, используя DOM
/*
* Те же самые операторы #include, которые использовались в примере 14.14
*/
using namespace std;
using namespace xercesc;
/*
* Определить XercesInitializer, как в примере 14.8,
* и CircusErorHandler, как в примере 14.7
*/
int main() {
try {
// Инициализировать Xerces и сконструировать парсер DOM.
XercesInitializer init;
XercesDOMParser parser;
// Включить проверку
parser.setValidationScheme(XercesDOMParser::Val_Always);
parser.setDoSchema(true); parser.setDoNamespaces(true);
parser.setExternalNoNamespaceSchemaLocation(
fromNative("animals.xsd").c_str());
// Зарегистрировать обработчик ошибок для получения уведомлений о
// нарушениях схемы
CircusErrorHandler handler;
parser.setErrorHandler(&handler);
// Выполнить синтаксический анализ и проверить соответствие документа
// схеме.
parser parse("animals.xml");
} catch (const SAXException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const XMLException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
Подобно определениям DTD, рассмотренным в предыдущем рецепте, схемы накладывают ограничения на документы XML. Схема предназначена для определения подмножества правильно сформированных документов, характерных для определенной прикладной области. Однако схемы имеют три отличия от определений DTD. Во-первых, концепция DTD и связанное с ней понятие подтверждения достоверности (validity) определены в самой спецификации XML, в то время как схемы описаны в другой спецификации — в рекомендациях XML Schema. Во-вторых, сами схемы являются правильно сформированными документами XML, в то время как для описания определений DTD используется специальный синтаксис, продемонстрированный в примере 14.11. В-третьих, схемы существенно более выразительны, чем определения DTD. Из-за двух последних отличий считается, что схемы превосходят определения DTD.
Например, в DTD из примера 14.11 можно было лишь потребовать, чтобы элементы
veterinarian
имели ровно два атрибута, name
и phone
, значения которых состоят из символов. Напротив, схема в примере 14.16 требует, чтобы значение атрибута phone
, кроме того, соответствовало регулярному выражению \(\d{3}\)\d{3}-\d{4}
, т.е. чтобы оно имело вид (ddd)xxx-dddd
, где d
может быть любой цифрой. Аналогично обстоит дело с элементом dateOfBirth
: если в DTD можно было только потребовать, чтобы этот элемент имел текстовое значение, то схема требует, чтобы текстовое значение имело вид yyyy-mm-dd
, где yyyy
задается в диапазоне от 0001 до 9999, mm
— от 01 до 12, a dd
— от 01 до 31.
Способность накладывать эти дополнительные ограничения создает большое преимущество, поскольку позволяет часть программистской работы переложить на парсер.
Рецепт 14.5.
Требуется преобразовать документ XML, используя таблицу стилей XSLT.
Используйте библиотеку Xalan. Во-первых, сконструируйте экземпляр конвертора XSTL
xalanc::XalanTransformer
. Затем сконструируйте два экземпляра xalanc::XSLTInputSource
(один для документа, который будет преобразован, а другой для вашей таблицы стилей) и экземпляр хаlanc::XSLTResultTarget
для документа, который будет получен в результате преобразования. Наконец, вызовите метод XSLT transform()
, передавая в качестве аргументов два экземпляра XSLTInputSource
и один XSLTResultTarget
.
Например, представим, что требуется с помощью веб-браузера просматривать список животных цирка из примера 14.1. Это легко сделать с помощью XSLT В примере 14.19 приводится таблица стилей XSLT, которая на входе принимает документ XML, такой как animals.xml, и формирует документ HTML, содержащий таблицу, в каждой строке которой описывается одно животное с указанием клички, вида, даты рождения, ветеринара и дрессировщика. Пример 14.20 показывает, как можно использовать библиотеку Xalan, чтобы воспользоваться этой таблицей стилей для документа animals.xml. В примере 14.21 приводится HTML, сгенерированный программой из примера 14.20; этот HTML переформатирован для лучшего восприятия.
Пример 14.19. Таблица стилей для animals.xml
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
Feldman Family Circus Animals
Feldman Family Circus Animals
Name
Species
Date of Birth
Veterinarian
Trainer
name:
phone:
Пример 14.20. Применение таблицы стилей animals.xsl для файла animals.xml с использованием библиотеки Xalan
#include
#include // cout
#include
#include
#include
#include
#include
#include "xerces_strings.hpp" // Пример 14.4
using namespace std;
using namespace xercesc;
using namespace xalanc;
// Утилита RAII, которая инициализирует парсер и освобождает ресурсы
// при выходе из области видимости
struct XalanInitializer {
XalanInitializer() {
XMLPlatformUtils::Initialize();
XalanTransformer::initialize();
}
~XalanInitializer() {
XalanTransformer::terminate();
XMLPlatformUtils::Terminate();
}
};
int main() {
try {
XalanInitializer init; // Инициализировать Xalan.
XalanTransformer xslt; // Конвертор XSLT.
XSLTInputSource xml("animals.xml"); // Документ XML из
// примера 14.1
XSLTInputSource xsl("animals.xsl"); // Таблица стилей из
// примера 14.19.
XSLTResultTarget html("animals.html"); // Результат выполнения xslt.
// Выполнить преобразование.
if (xslt.transform(xml, xsl, html) != 0) {
cout << "xml error: " << xslt.getLastError() << "\n";
}
} catch (const XMLException& e) {
cout << "xml error " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
Пример 14.21. Документ HTML, сгенерированный программой из примера 14.20
<МЕТА http-equiv="Content Type" content="text/html; charset=UTF-8">
Feldman Family Circus Animals
Feldman Family Circus Animals
Name
Species
Date of Birth
Veterinarian
Trainer
Herby
elephant
1992-04-23
name: Dr. Hal Brown
phone: (801)595-9627
name: Bob Fisk
phone: (801)881-2260
Sheldon
parrot
1998-09-30
name: Dr. Kevin Wilson
phone: (801)466-6498
name: Eli Wendel
phone: (801)929-2506
Dippy
penguin
2001-06-08
name: Dr. Barbara Swayne
phone: (801)459-7746
name: Ben Waxman
phone: (801)882-3549
XSL-преобразование (стандарт XSLT) представляет собой язык преобразования документов XML в другие документы XML. XSLT является одним из элементов семейства спецификаций расширяемых языков описания таблиц стилей (Extensible Stylesheet Language — XSL), который обеспечивает базовые средства для визуального представления документов XML Однако XSLT полезен не только при форматировании; например, он используется веб-серверами при генерации HTML-документов «на лету» и такими системами генерации документов, как DocBook.
Преобразования XSLT представляются в виде документов XML, называемых таблицами стилей (stylesheets). Таблица стилей используется для обработки исходного документа и формирования выходного документа (result document). Таблица стилей состоит из набора шаблонов, которым соответствуют узлы исходного документа и которые применяются для получения фрагментов выходного документа. Шаблоны рекурсивно применяются к исходному документу, генерируя фрагменты выходного документа один за другим, пока не будет обнаружено ни одного соответствия. Условия соответствия записываются с помощью языка XPath, предназначенного для извлечения информационных строк, чисел, булевых значений и наборов узлов из документов XML.
Таблица стилей представленная в примере 14.19, состоит из трех шаблонов. В главном шаблоне атрибут
match
равен /
, т.е. он соответствует корню исходною документа, а именно узлу, который является родительским узлом по отношению к корневому элементу документа и любым инструкциям обработки и комментариям верхнего уровня. При применении этого шаблона генерируется фрагмент документа HTML, содержащий заголовок «Животные цирка Feldman Family Circus» и таблицу с одной строкой, состоящей из пяти элементов th
с метками Name
, Species
, Date of Birth
, Veterinarian
и trainer
. Этот шаблон содержит элемент apply-templates
, которому соответствует атрибут animal
. Это приводит к тому, что второй шаблон таблицы стилей с атрибутом соответствия animal
— будет применяться один раз к каждому элементу animal
, дочернему по отношению к корневому документу, формируя строку таблицы для каждого дочернего элемента. Строка, сгенерированная для элемента animal
, состоит из пяти элементов td
. Первые три элемента td
содержат текстовое значение дочерних элементов animal
(name
, species
и dateOfBirth
), извлекаемое с помощью инструкции XSLT value-of
. Последние два элемента td
содержат элементы таблицы, полученные путем применения третьего шаблона таблицы стилей с атрибутом соответствия veterinarian|trainer
, применяемого к дочерним элементам животного veterinarian
и trainer
.
Хотя в примере 14.20 мною указаны локальные файлы для таблицы стилей, исходного документа и выходного документа,
XSLTInputSources
и XSLTResultTargets
могут быть сконструированы из потоков стандартной библиотеки C++, позволяя XalanTransformer
принимать поток ввода и генерировать результат в произвольном месте. Более того, вместо получения на входе экземпляров XSLTInputSource
конвертор XalanTransformer
может работать с предварительно скомпилированной таблицей стилей, представляющей экземпляр xalanc::XalanCompiledStylesheet
, и с исходным документом, прошедшим обработку парсером и представленным экземпляром xalanc::XalanParsedSource
. Это проиллюстрировано в примере 14.22. Если требуется применять одну таблицу стилей к нескольким исходным документам, гораздо более эффективный результат получается при использовании XalanCompiledStylesheet
, чем XSLTInputSource
.
Пример 14.22. Выполнение преобразования XSLT с применением предварительно откомпилированной таблицы стилей
/*
* те же операторы #include, которые использовались в примере 14.20
*/
using namespace std;
using namespace xercesc;
using namespace xalanc;
/*
* Определить XalanInitializer так же, как в примере 14.20
*/
int main() {
try {
XalanInitializer init; // Инициализировать Xalan
XalanTransformer xslt; // Конвертор XSLT.
XSLTResultTarget html("animals.html"); // Результат работы xslt.
// Выполнить синтаксический анализ исходного документа
XSLTInputSource xml("animals.xml");
XalanParsedSource* parsedXml = 0;
if (xslt.parseSource(xml, parsedXml) != 0) {
cout << "xml error: " << xslt.getLastError() << "\n";
}
// Компилировать таблицу стилей.
XSLTInputSource xsl("animals.xsl");
XalanCompiledStylesheet* compiledXsl = 0;
if (xslt.compileStylesheet(xsl, compiledXsl) != 0) {
cout << "xml error: " << xslt.getLastError() << "\n";
}
// Выполнить преобразование.
if (xslt.transform(xml, xsl, html)) {
cout << "xml error: " << xslt.getLastFrror() << "\n";
}
} catch (const XMLException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
Рецепт 14.8.
Требуется извлечь информацию из документа XML, обработанного парсером, путем вычисления XPath-выражения.
Используйте библиотеку Xalan. Во-первых, выполните синтаксический анализ документа XML и получите указатель на
xalanc::XalanDocument
. Это можно сделать, используя экземпляры XalanSourceTreeInit
, XalanSourceTreeDOMSupport
и XalanSourceTreeParserLiaison
, каждый из которых следующим образом определяется в пространстве имен xalanc
.
#include
#include
#include
#include
...
int main() {
...
// Инициализировать подсистему XalanSourceTree
XalarSourceTreeInit init;
XalanSourceTreeDOMSupport support;
// Интерфейс доступа к парсеру
XalanSourceTreeParserLiaison liaison(support);
// Подключить DOMSupport к ParserLiaison
support.setParserLiaison(&liaison);
LocalFileInputSource src(место-расположения-документа);
XalanDocument* doc = liaison.ParseXMLStream(doc);
...
}
Можно поступить по-другому и использовать парсер Xerces DOM для получения указателя на
DOMDocument
, как это сделано в примере 14.14, и затем использовать экземпляры XercesDOMSupport
, XercesParserLiaison
и XercesDOMWrapperParsedSource
, каждый из которых определяется в пространстве имен xalanc
для получения указателя на XalanDocument
, соответствующего документу DOMDocument
.
#include
#include
#include
#include
...
int main() {
...
DOMDocument* doc = ...;
XercesDOMSupport support;
XercesParserLiaison liaison(support);
XercesDOMWrapperParsedSource src(doc, liaison, support);
XalanDocument* xalanDoc = src.getDocument();
...
}
На следующем шаге получите указатель на узел, выполняющий роль узла контекста при вычислении выражения XPath. Это можно сделать с помощью интерфейса DOM документа
XalanDocument
. Сконструируйте XPathEvaluator
для вычисления выражения XPath и XalanDocumentPrefixResolver
для разрешения префиксов пространств имен в документе XML. Наконец, вызовите метод XPathEvaluator::evaluate()
, передавая в качестве аргументов DOMSupport
, контекстный узел, XPath-выражение и PrefixResolver
. Результат вычисления выражения возвращается в виде объекта типа XObjectPtr
; тип допустимых операций над этим объектом зависит от типа его данных XPath, который можно узнать при помощи метода getType()
.
Например, пусть требуется извлечь список имен животных из документа animals.xml, представленного в примере 14.1. Вы можете это сделать, выполняя синтаксический анализ документа и вычисляя XPath-выражение
animalList/animal/name/child::text()
с использованием корня документа в качестве контекстного узла. Это проиллюстрировано в примере 14.23.
Пример 14.23. Вычисление ХРаth-выражения, используя Xalan
#include // size_t
#include
#include // cout
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "animal.hpp"
#include "xerces_strings.hpp"
using namespace std;
using namespace xercesc;
using namespace xalanc;
// Утилита RAII, которая инициализирует парсер и процессор XPath, освобождая
// ресурсы при выходе из области видимости
class XPathInitializer {
public:
XPathInitializer() {
XMLPlatformUtils::Initialize();
XPathEvaluator::initialize();
}
~XPathInitializer() {
XpathEvaluator::terminate();
XMLPlatformUtils::Terminate();
}
private:
// Запретить копирование и присваивание
XPathInitializer(const XPathInitializer&);
XPathInitializer& operator=(const XPathInitializer&);
};
// Получает уведомления об ошибках
class CircusErrorHandler : public DefaultHandler {
public:
void error(const SAXParseException& e) {
throw runtime_error(toNative(e.getMessage()));
}
void fatalError(const SAXParseException& e) { error(e); }
};
int main() {
try {
// Инициализировать Xerces и XPath и сконструировать парсер DOM.
XPathInitializer init;
XercesDOMParser parser;
// Зарегистрировать обработчик ошибок
CircusErrorHandler error;
parser.setErrorHandler(&error);
// Выполнить синтаксический анализ animals.xml.
parser.parse(fromNative("animals.xml").c_str());
DOMDocument* doc = parser.getDocument();
DOMElement* animalList = doc->getDocumentElement();
// Создать XalanDocument на основе doc.
XercesDOMSupport support;
XercesParserLiaison liaison(support);
XercesDOMWrapperParsedSource src(doc, liaison, support);
XalanDocument* xalanDoc = src.getDocument();
// Вычислить XPath-выражение для получения списка
// текстовых узлов, содержащих имена животных
XPathEvaluator evaluator;
XalanDocumentPrefixResolver resolver(xalanDoc);
XercesString xpath =
fromNative("animalList/animal/name/child::text()");
XObjectPtr result =
evaluator.evaluate(
support, // поддержка DOM
xalanDoc, // контекстный узел
xpath.c_str(), // XPath-выражение
resolver); // функция разрешения пространства имен
const NodeRefListBase& nodeset = result->nodeset();
// Просмотр списка узлов и вывод имен животных
for (size_t i = 0, len = nodeset.getLength(); i < len; ++i) {
const XMLCh* name = nodeset.item(i)->getNodeValue().c_str();
std::cout << toNative(name) << "\n";
}
} catch (const DOMException& e) {
cout << "xml error: " << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
XPath — это язык поиска по образцу (pattern matching language), предназначенный для извлечения информации из документов XML. Основная конструкция XPath — выражение пути (path expression) поддерживает иерархический синтаксис ссылок на элементы, атрибуты и текстовые узлы на основе использования их имен, атрибутов, текстового содержимого, отношений наследования и других свойств. Кроме работы с наборами узлов язык XPath может обрабатывать строки, числа и булевы значения. XPath версии 2.0, которая в настоящее время не поддерживается библиотекой Xalan, использует даже более сложную модель данных, основанную на рекомендациях XML Schema. (См. рецепт 14.5.)
XPath-выражения вычисляются в контексте узла документа XML, называемого контекстным узлом, который используется для интерпретации связанной с ним конструкции, например,
parent
, child
и descendant
. В примере 14.23 я указал корень (root
) документа XML в качестве контекстного узла; этот узел является родительским по отношению к корневому элементу документа XML, а также к любой инструкции обработки и комментариям верхнего уровня. При вычислении выражения с использованием корневого узла в качестве контекстного узла выражение пути animalList/animal/name/child::text()
соответствует всем текстовым узлам, дочерним по отношению к элементам name, родительским элементом которых является animal
, и чьим «дедушкой» является элемент animalList
.
Метод
evaluate()
класса XPathEvaluator
возвращает XObjectPtr
, представляющий результат вычисления выражения XPath. Тип данных, на который ссылается XObjectPtr
, можно узнать путем его разыменования с получением XObject
и вызова метода getType()
; затем можно получить доступ к базовым данным при помощи вызова num()
, boolean()
, str()
или nodeset()
. Поскольку XPath-выражение в примере 14.23 представляет набор узлов, я использовал метод nodeset()
для получения ссылки на NodeRefListBase
, который обеспечивает доступ к узлам в наборе с помощью его методов getLength()
и item()
. Метод item()
возвращает указатель на узел XalanNode
, метод getNodeValue()
которого возвращает строку с интерфейсом, похожим на интерфейс std::basic_string
.
Поскольку XPath обеспечивает простой способ определения местоположения узлов в документе XML, возникает естественный вопрос о возможности применения выражений Xalan XPath для получения экземпляров
xercesc::DOMNode
из xercesc::DOMDocument
. На самом деле это возможно, но не совсем удобно, а кроме того, по умолчанию узлы xercesc::DOMNodes
, полученные таким способом, представляют дерево документа XML с возможностями только чтения, что уменьшает пользу от применения XPath в качестве средства манипулирования DOM. Существуют способы, позволяющие обойти это ограничение, однако они достаточно сложны и потенциально опасны.
К счастью, библиотека Pathan реализует XPath, совместимый с Xerces и позволяющий легко манипулировать Xerces DOM. Пример 14.24 показывает, как можно использовать Pathan для определения места расположения и удаления узла слона Herby из документа XML, приведенного в примере 14.1, с помощью вычисления XPath-выражения
animalList/animal[child::name='Herby']
. Сравнение этого примера с примером 14.10 ясно показывает, насколько мощным является язык XPath.
Пример 14.24. Определение местоположения узла и удаление его с использованием библиотеки Pathan
#include
#include // cout
#include
#include
#include
#include
#include
#include
#include "xerces_strings.hpp" // Пример 14.4
using namespace std;
using namespace xercesc;
/*
* Определить XercesInitializer, как это сделано в примере 14.8, а также
* CircusFrrorHandler и DOMPtr, как это сделано в примере 14.10
*/
int main() {
try {
// Инициализировать Xerces и получить DOMImplementation.
XercesInitializer init;
DOMImplementation* impl =
DOMImplementationRegistry::getDOMImplementation(
fromNative("LS").c_str()
);
if (impl == 0) {
cout << "couldn't create DOM implementation\n";
return EXIT_FAILURE;
}
// Сконструировать DOMBuilder для синтаксического анализа
// документа animals.xml.
DOMPtr parser =
static cast(impl)-> createDOMBuilder(
DOMImplementationLS::MODE_SYNCHRONOUS, 0
);
CircusErrorHandler err;
parser->setErrorHandler(&err);
// Выполнить синтаксический анализ
animals.xml. DOMDocument* doc =
parser->parseURI("animals.xml");
DOMElement* animalList = doc->getDocumentElement();
// Создать XPath-выражение.
auto_ptr
evaluator(XPathEvaluator::createEvaluator());
auto_ptr
resolver(evaluator->createNSResolver(animalList));
auto_ptr xpath(
evaluator->createExpression(FromNative(
"animalList/animal[child::name='Herby']" ).c_str(), resolver.get()
)
);
auto_ptr evaluator(XPathEvaluator::createEvaluator());
auto_ptr resolver(evaluator->createNSResolver(animalList));
auto_ptr xpath(evaluator->createExpression(
fromNative("animalList/animal[child::name='Herby']").c_str(),
resolver.get()
));
// Вычислить выражение.
XPathResult* result = xpath->evaluate(doc,
XPathResult::ORDERED_NODE_ITERATOR_TYPE, 0
);
DOMNode* herby;
if (herby = result->iterateNext()) {
animalList->removeChild(herby);
herby->release(); // optional
}
// Сконструировать DOMWriter для сохранения animals.xml
DOMPtr writer =
static_cast(impl)->createDOMWriter();
writer->setErrorHandler(&err);
// Сохранить animals.xml.
LocalFileFormatTarget file("circus.xml");
writer->writeNode(&file, *animalList);
} catch (const DOMException& e) {
cout << toNative(e.getMessage()) << "\n";
return EXIT_FAILURE;
} catch (const XPathException &e) {
cout << e.getString() << "\n";
return EXIT_FAILURE;
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
Пример 14.24 использует Pathan 1, который реализует рекомендации XPath 1.0; библиотекой Xalan в настоящее время поддерживается именно эта версия. Pathan 2, который в настоящее время доступен в бета-версии, обеспечивает предварительную реализацию рекомендаций XPath 2.0. Pathan 2 представляет собой более точную реализацию стандарта XPath; я рекомендую использовать Pathan 2 вместо Pathan 1, как только станет доступна не бета-версия.
Рецепт 14.7.
Требуется иметь возможность сохранения набора объектов C++ в документе XML и считывания их потом обратно в память.
Используйте библиотеку Boost Serialization. Эта библиотека позволяет сохранять и восстанавливать объекты, используя классы, называемые архивами. Для использования этой библиотеки вы должны сначала сделать каждый из ваших классов сериализуемым (serializable), что просто означает возможность записи экземпляров класса в архив (это называется сериализацией) и их обратного считывания в память (это называется десериализацией). Затем на этапе выполнения вы можете сохранить ваши объекты в архиве XML, используя оператор
<<
, и восстановить их, используя оператор >>
.
Чтобы сделать класс сериализуемым, добавьте шаблон функции-члена
serialize
со следующей сигнатурой.
template
void serialize(Archive& ar, const unsigned int version);
В реализации
serialize
необходимо обеспечить запись каждого данного-члена класса в указанный архив в виде пары «имя-значение», используя оператор &
. Например, если вы хотите сериализовать и десериализовать экземпляры класса Contact
из примера 14.2, добавьте функцию-член serialize
, как это сделано в примере 14.25.
Пример 14.25. Добавление поддержки сериализации в класс Contact из примера 14.2
#include // пара "имя-значение"
class Contact {
...
private:
friend class boost::serialization::access;
template
void serialize(Archive& ar, const unsigned int version) {
// Записать (или считать) каждое данное-член в виде пары имя-значение
using boost::serialization::make_nvp;
ar & make_nvp("name", name_);
ar & make_nvp("phone", phone_);
}
...
};
Аналогично можно обеспечить сериализацию класса
Animal
из примера 14.2, как это сделано в примере 14.26.
Пример 14.26. Добавление поддержки сериализации для класса Animal из примера 14.2
...
// Включить поддержку сериализации для boost::gregorian::date
#include
...
class Contact {
...
private:
friend class boost::serialization::access;
template
void serialize(Archive& ar, const unsigned int version) {
// Записать (или считать) каждое данное-член в виде пары имя-значение
using boost::serialization::make_nvp;
ar & make_nvp("name", name_);
ar & make_nvp("species", species_);
ar & make_nvp("dateOfBirth", dob_);
ar & make_nvp("veterinarian", vet_);
ar & make_nvp("trainer", trainer_);
}
...
};
Теперь вы можете сериализовать Animal, создавая архив XML типа
boost::archive::xml_oarchive
и записывая информацию о животном в архив, используя оператор <<
. Конструктор xml_oarchive
в качестве аргумента принимает std::ostream
; часто этим аргументом будет поток вывода, используемый для записи в файл, однако в общем случае для записи данных может использоваться ресурс любого типа. После сериализации экземпляра Animal
его можно считать обратно в память, конструируя архив XML типа boost::archive::xml_iarchive
, подключая его к тому же самому ресурсу, который использовался первым архивом, и применяя оператор >>
.
Пример 14.27 показывает, как можно использовать Boost.Serialization для сохранения вектора
std::vector
, состоящего из объектов Animal
, в файле animals.xml и затем для загрузки его обратно в память. В примере 14.28 показано содержимое файла animals.xml после выполнения программы из примера 14.27.
Пример 14.27 Сериализация вектора std::vector, состоящего из объектов Animal
#include
#include // Архив для записи XML
#include // Архив для чтения XML
#include // Средства сериализации вектора
#include "animal.hpp" // std::vector
int main() {
using namespace std;
using namespace boost::archive; // пространство имен для архивов
using namespace boost::serialization; // пространство имен для make_nvp
try {
// Заполнить список животных
vector animalList;
animalList.push_back(
Animal("Herby", "elephant", "1992-04-23",
Contact("Dr. Hal Brown", "(801)595-9627"),
Contact("Bob Fisk", "(801)881-2260")));
animalList.push_back(
Animal("Sheldon", "parrot", "1998-09-30",
Contact("Dr. Kevin Wilson", "(801)466-6498"),
Contact("Eli Wendel", "(801)929-2506")));
animalList.push_pack(
Animal("Dippy", "penguin", "2001-06-08",
Contact("Dr. Barbara Swayne", "(801)459-7746"),
Contact("Ben Waxman", "(801)882-3549")));
// Сконструировать выходной архив XML и сериализовать список
ofstream fout("animals.xml");
xml_oarchive oa(fout);
oa << make_nvp("animalList", animalList);
fout.close();
// Сконструировать входной архив XML и десериализовать список
ifstream fin("animals.xml");
xml_iarchive ia(fin);
vector animalListCopy;
ia >> make_nvp("animalList", animalListCopy);
fin.close();
if (animalListCopy != animalList) {
cout << "XML serialization failed\n";
return EXIT_FAILURE;
}
} catch (const exception& e) {
cout << e.what() << "\n";
return EXIT_FAILURE;
}
}
Пример 14.28. Файл animals.xml после выполнения программы из примера 14.27
3
Herby
elephant
19920423
Dr. Hal Brown
(801)595-9627
Bob Fisk
(801)881-2260
Sheldon
parrot
19980930
Dr. Kevin Wilson
(801)466-6498
Eli Wendel
(801)929-2506
Dippy
penguin
20010608
Dr. Barbara Swayne
(801)459-7746
Ben Waxman
(801)882-3549
Библиотека Boost Serialization обеспечивает наиболее изощренный и гибкий способ сохранения и восстановления объектов C++. Она представляет собой очень сложный фреймворк. Например, она позволяет сериализовать сложные структуры данных, содержащие циклические ссылки и указатели на полиморфные объекты. Более того, применение этой библиотеки совсем не ограничивается сериализацией XML: кроме архивов XML она предоставляет несколько типов текстовых и бинарных архивов. Архивы XML и текстовые архивы являются переносимыми, т.е. данные можно сериализовать в одной системе и десериализовать в другой; бинарные архивы не переносимы, но компактны.
Нет никаких спецификаций, которым соответствовали бы документы XML, полученные при помощи Boost.Serialization, и их формат может изменяться в новых версиях Boost. Поэтому вы не можете использовать эти документы совместно с другими фреймворками сериализации С++. Тем не менее XML-сериализация приносит пользу, потому что сериализованный вывод легко воспринимается человеком и может обрабатываться инструментальными средствами, ориентированными на XML.
Примеры 14.25 и 14.26 демонстрируют интрузивную сериализацию (intrusive serialization): классы
Animal
и Contact
были модифицированы, чтобы обеспечить их сериализацию. Boost.Serialization
также поддерживает неинтрузивную сериализацию (nonintrusive serialization), обеспечивая сериализацию классов без модификации их определений при условии доступности всех состояний объекта через его открытый интерфейс. Вы уже видели пример неинтрузивной сериализации в примере 14.27: шаблон std::vector
допускает сериализацию, несмотря на то что его определение не может модифицироваться конечными пользователями. Фактически все контейнеры стандартной библиотеки являются сериализуемыми; для обеспечения сериализации контейнера, определенного в стандартном заголовочном файле xxx
, просто включите заголовочный файл boost/serialization/xxx.hpp. Дополнительную информацию о неинтрузивной сериализации вы можете найти в документации Boost.Serialization.
Примеры 14.25 и 14.26 иллюстрируют также двойственную роль оператора
&
: он действует как оператор <<
при сериализации объекта и как оператор >>
при десериализации объекта. Это удобно, потому что позволяет реализовать сериализацию и десериализацию одной функцией. Однако в некоторых случаях неудобно использовать одну функцию для сериализации и десериализации; для этого в Boost.Serialization предусмотрен механизм разделения метода serialize()
на два отдельных метода, load()
и save()
. Если вам необходимо воспользоваться преимуществами этой возможности, обратитесь к документации Boost.Serialization.
В примерах 14.25, 14.26 и 14.27 я использую функцию
boost::serialization::make_nvp
для конструирования пар вида «имя-значение». В Boost.Serialization предусмотрен также макрос BOOST_SERIALIZATION_NVP
, который позволяет выполнять сериализацию переменной, указывая ее имя. Первый компонент пары будет сконструирован автоматически препроцессором, используя оператор «стрингизации» (stringizing) #
для преобразования макропараметров в строковые константы.
// То же самое, что и ar & make_nvp("name_", name_);
ar & BOOST_SERIALIZATION_NVP(name_);
В этих примерах я использую
make_nvp
вместо BOOST_SERIALIZATION_NVP
для лучшего контроля имен тегов, чтобы содержимое архива XML легче читалось.
В документации Boost.Serialization рекомендуется объявлять метод
serialize()
как закрытый (private
) для уменьшения ошибок пользователя, когда добавляется поддержка сериализации в классы, производные от других сериализуемых классов. Для того чтобы библиотека Boost.Serialization могла вызвать метод serialize()
вашего класса, вам необходимо объявить дружественным класс boost::serialization::access
.
Наконец второй параметр метода
serialize()
в примерах 14.25 и 14.26 относится к той части Boost.Serialization, которая поддерживает управление версиями классов (class versioning). Когда объект определенного класса первый раз сохраняется в архиве, вместе с ним сохраняется также его версия; когда выполняется десериализация экземпляра класса. Boost.Serialization передает сохраненную версию методу serialize
в качестве второго аргумента. Эта информация может использоваться для специализации десериализации; например, serialize
мог бы загружать переменную-член только в том случае, если записанная в архив версия класса, по крайней мере, не меньше версии класса, первым объявившим эту переменную. По умолчанию класс имеет версию 0. Для задания версии класса вызовите макрос BOOST_CLASS_VERSION
, который определен в заголовочном файле boost/serialization/version.hpp, передавая в качестве аргументов имя и версию класса.