Объектное представление XML-документовАвтор: sergek.
Март, 2010
ВведениеВ данной статье предлагается простой и достаточно универсальный способ работы с XML-документами в программах C++ с использованием SAX-анализатора, приводятся примеры его использования.
Подход был разработан при реализации библиотеки классов C++ для работы с XML-документами специализированных форматов (
http://www.freesoft.ru/?id=680219). Библиотека была предназначена для проектов Qt, поэтому предлагаемый способ также опирается на средства Qt. Соответственно, приводимые здесь примеры взяты из упомянутого проекта. Но, поскольку интерфейс SAX хорошо стандартизован, этот подход можно перенести на другие реализации SAX-анализатора.
Термин «объектное представление» XML-документов, используемый в данной статье, означает то, что содержимое документов описывается в программе C++ в виде классов, и работа с XML-документами, элементами и атрибутами документа в программе сводится к работе с объектами и членами-данными этих объектов. Далее для простоты вместо термина «член-данное» будем использовать «реквизит».
Статья ориентирована на программистов, знакомых с объектно-ориентированным программированием на C++ и принципами работы SAX-анализатора.
Достоинства и ограничения подходаГлавное достоинство предлагаемого способа заключается в чрезвычайной простоте работы с XML-документом. В качестве примера приведем элементарный пример работы со следующим исходным текстом:
<?xml version="1.0" encoding="windows-1251"?>
<ED EDNo="805253" EDDate="2005-03-01" EDAuthor="4552000000"/>
Фрагмент программного кода, показывающего работу с атрибутами документа:
// объект
CED ed;
// 1. чтение документа (инициализация реквизитов объекта)
ed.readDocument(fileName);
// 2. изменение реквизитов
ed.EDNo = "1";
ed.EDDate = "2010-03-22";
ed.EDAuthor = "4552000001";
// 3. запись измененого XML-документа
ed.writeDocument(fileName);
Выходной документ:
<?xml version="1.0" encoding="windows-1251"?>
<ED EDNo="1" EDDate="2010-03-22" EDAuthor="4552000001"/>
Этот пример хорошо иллюстрирует цепочку преобразований «XML –> объект –> XML», обеспечивающую последовательное чтение, изменение и запись XML-документа. Объект в середине этой цепочки является представлением документа в удобном для использования в прикладных программах виде.
Естественно, за любое удобство надо платить. В данном случае платой является то, что с помощью таких объектов можно работать только с документами заранее известной структуры. При изменении структуры документов необходимо, кроме участков кода, где используются реквизиты объекта, менять и само объявление класса, описывающего представление документа.
Здесь мы намеренно не касаемся вопросов эффективного использования оперативной памяти – это отдельная задача, которая должна решаться для каждого конкретного случая. Во всяком случае, автору представляется, что предложенное решение в этом отношении ничуть не хуже, чем использование DOM, но обладает большей гибкостью и удобством использования.
Общие принципы объектного представленияСтруктура класса повторяет структуру XML-документаСамо по себе использование объекта для представления XML-документа никакого выигрыша не дает, все дело в том,
как инициализировать реквизиты объекта. Те примеры, которые приведены в [1] или в составе Qt SDK оптимизма не вселяли – организация работы по использованию данных документа в этих примерах возлагалось на обработчики SAX-анализатора: startElement(),endElement() и characters(). Естественно, такое решение для работы с большим набором различных форматов XML-документов не подходило.
Поэтому сразу появилась мысль всю работу по чтению (инициализации) и записи объектов возложить на сами объекты, а обработчики парсера сделать независимыми от формата исходного документа. Сделать это достаточно просто, используя такие замечательные свойства C++, как наследование и полиморфизм. А третий «кит» объектно-ориентированного языка (инкапсуляция) позволит так реализовать классы объектного представления, что будущее (неизбежное!) изменение формата документов уже не будет представляться такой уж сложной задачей.
Итак, вспомним, как SAX-анализатор выполняет разбор XML-документа – он начинает с верхнего (корневого) узла и проходит по дереву, в узлах которого находятся элементы XML-документа. Когда встречается открывающий тег элемента, происходит вызов обработчика startElement(), куда передается список значений атрибутов этого элемента, когда парсер достигает закрывающего тега – вызывается endElement(). Обработка символьных данных выполняется иначе, но, как будет показано ниже, эти отличия не играют существенной роли.
Для выполнения инициализации реквизитов объекта, необходимо, чтобы для каждого структурного элемента XML-документа был поставлен в соответствие структурный элемент класса, описывающего представление. Иными словами, необходимо, чтобы структура класса повторяла структуру XML-документа. Это легко выполнить, если потребовать, чтобы при конструировании классов каждый элемент (узел) исходного документа отображался в свой класс, который назовем
узловым классом.
Атрибуты или текстовые элементы исходного документа реализуются в классе в виде членов-данных, вложенные элементы исходного документа – в виде объектов других узловых классов. Как правило, если XML-документ был спроектирован правильно, каждый узловой класс представляет собой некую сущность предметной области, поэтому узловые классы еще называют
прикладными.
И, наконец, если у каждого узлового класса будет общий предок, на которого возложим интерфейсные функции, то нетрудно обеспечить, чтобы из обработчиков вызывались соответствующие методы этого интерфейсного класса. Для этого обработчики должны оперировать указателем на интерфейсный класс (да здравствует полиморфизм!).
Узловые классы имеют общего предка Интерфейс между парсером и объектным представлением XML-документа обеспечивается специальным классом, который, как уже было указано выше, должен быть предком всех узловых (прикладных) классов. Требования к интерфейсному классу (назовем его CNode, префикс «C» от англ. class) диктуются спецификацией SAX-анализатора.
Во-первых, самое очевидное:
· интерфейсный класс должен предоставить метод инициализации (присвоения) реквизитов объекта.
Атрибуты и текстовые элементы (символьные данные) отображаются в объектном представлении одинаково – в виде реквизитов (членов-данных) класса. Однако обрабатываются они по-разному: атрибуты – в обработчике startElement(), символьные данные – в обработчике endElement(). Дело в том, что парсер передает программе символьные данные посредством обработчика characters(), однако уверенность в том, что данные были переданы полностью, появляется только при достижении парсером конца элемента, содержащего эти данные. Для того, чтобы вызвать интерфейсный метод инициализации для текстового элемента, необходимо знать, что тип этого элемента – текстовый. Таким образом, можно сформулировать второе требование к интерфейсу:
· в интерфейсе должен быть предусмотрен метод индикации текстовых элементов. Он должен выполнять простую задачу – по имени элемента сообщить, является ли он символьным или нет.
Получив в обработчике endElement() информацию о том, что текущий элемент был символьным, можно смело вызывать метод инициализации реквизитов.
И, наконец, обработчики должны обращаться к методам конкретного объекта (или его структурной части). Начинается разбор всегда с корневого узла, но по мере продвижения по дереву документа, должен меняться указатель на текущий узел объекта. Таким образом:
· интерфейсный класс должен иметь метод получения указателя на текущий узел объекта.
Если текущий узел объекта не содержит других объектов, то метод просто возвращает this. В противном случае указатель инициализируется на нужный вложенный узел. Последнее требование должно сопровождаться организацией в обработчиках парсера стека указателей таким образом, чтобы обработчики всегда работали с текущим узлом объекта.
Сформулированные выше требования относятся к взаимодействию объектного представления с SAX-анализатором в процессе чтения (разбора) XML-документа.
Запись документа может выполняться с использованием любых средств, предоставляемых выбранным средством программирования. В Qt такие достаточно удобные средства предоставляет класс QXmlStreamWriter. Реализация записи, учитывая древовидную природу XML, должна быть распределена по иерархии объектного представления, поэтому в интерфейс выделяем еще один метод:
· метод записи узлового объекта в XML-документ.
Итак, для обеспечения интерфейса с парсером и классом записи документа в интерфейсе CNode должны быть предусмотрены четыре виртуальных метода. Все эти методы должны иметь реализацию по умолчанию, чтобы в порожденных классах выполнять определение только тех методов, какие действительно необходимы. Взаимодействие объектов с парсером осуществляется через средства, представляемые CNode, с обработчиками парсера, которые в данном случае выполнены в виде класса CSaxHandler.
Это, так сказать, обеспечение заявленного универсального подхода. О реализации этих двух классов – в следующей главе.
Реализация подходаОбъем кода, который обеспечивает реализацию предложенного подхода, не очень большой, поэтому он приведен в этом подразделе почти полностью. Из классов удалены лишь некоторые несущественные детали (например, флаги, специфичные для конкретной реализации, обработчик ошибок).
Исходные тексты, приведенные ниже, разбиты на два модуля – cnode.cpp и csaxhandler.cpp.
Интерфейсный класс CNodeКласс CNode является предком всех узловых классов объектного представления, включая корневой узел. Объявление этого класса следующее:
// cnode.h
#ifndef CNODE_H
#define CNODE_H
#include <QString>
//----------------------------------------------------------------------
// CNode - узел объекта
// Интерфейсный класс, обеспечивающий взаимодействие объекта и XML
//----------------------------------------------------------------------
// Forward Decls
class QXmlAttributes;
class QXmlStreamWriter;
class QIODevice;
class CNode
{
private:
// вспомогательные методы работы с устройствами записи/чтения
bool writeToDevice(QIODevice* device);
bool readFromDevice(QIODevice* device);
protected:
// пространство имен и префикс элемента
QString nodeNamespace;
QString nodePrefix;
// методы для записи в XML необязательных реквизитов
void writeAttribute(QXmlStreamWriter& writer,const QString& name, const QString& value);
void writeTextElement(QXmlStreamWriter& writer,const QString& nsUri,const QString& name,const QString& text);
// интерфейсные методы - используются для чтения из XML SAX-парсером
friend class CSaxHandler;
virtual void setRequisites(const QString &name,const QXmlAttributes &attributes);
virtual CNode* getNode(const QString &name);
virtual bool isTextElement(const QString &name);
// интерфейсный метод - запись объекта в XML
virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);
public:
CNode();
// наименование узла
QString nodeName;
// чтение объекта из XML - из файла или символьного массива
bool readDocument(const QString &fileName);
bool readDocument(QByteArray* array);
// запись объекта в XML - в файл или символьный массив
bool writeDocument(const QString &fileName);
bool writeDocument(QByteArray* array);
// флаги, используемые при записи
static QString encoding; // кодировка, используемая при записи
static bool autoFormatting; // флаг форматирования XML при записи
};
//----------------------------------------------------------------------
#endif // CNODE_H
Класс обработчиков парсера CSaxHandler объявлен дружественным, чтобы скрыть интерфейсные методы в защищенной области. Как ранее говорилось, интерфейс должен включать четыре метода:
· void setRequisites(const QString &name,const QXmlAttributes &attributes) – инициализация реквизитов объекта;
· CNode* getNode(const QString &name) – получение указателя на объект узлового класса. Метод должен возвращать указатель на объект в случае успеха или 0, если объект с именем name не существует;
· bool isTextElement(const QString &name) – метод индикации текстовых реквизитов, возвращает true, если реквизит с именем namе является текстовым, и false в противном случае;
· bool writeNode(QXmlStreamWriter& writer,const QString& nsUri) – запись реквизитов узлового класса. Реализация этого метода в прикладных классах зависит от того, какие средства используются для формирования XML-документа. Ниже приведен пример реализации с использованием класса Qt QXmlStreamWriter.
Интерфейсный класс обеспечивает методами readDocument() и writeDocument() чтение и запись XML-документа в файл или символьный массив QByteArray, которые подключаются в качестве устройств ввода/вывода. Символьный массив играет роль строки, но с более широкими возможностями работы с различными кодировками XML-документов.
Обратите внимание на реквизит nodeName: его необходимо инициализировать в конструкторах прикладных классов именем элементов XML-документов, отображением которых эти классы являются.
Определение класса CNode также не отличается чрезмерной сложностью. Как уговаривались, для базового класса все интерфейсные методы имеют реализации по умолчанию, позволяющие не определять их в наследниках, если в этом нет необходимости:
// cnode.cpp
#include "cnode.h"
#include "cnode.h"
#include "csaxhandler.h"
#include <QFile>
#include <QBuffer>
#include <QXmlStreamWriter>
//----------------------------------------------------------------------
QString CNode::encoding = "WINDOWS-1251";
bool CNode::autoFormatting = true;
//----------------------------------------------------------------------
CNode::CNode(){
}
//----------------------------------------------------------------------
// интерфейсные методы
//----------------------------------------------------------------------
void CNode::setRequisites(const QString &name,const QXmlAttributes &attributes){
// ничего не делается - для классов, не содержащих реквизиты
}
// указатель на узел элемент
CNode* CNode::getNode(const QString &name){
if(name==nodeName)
return this;
else
return 0;
}
// проверка, является ли элемент текстовым
bool CNode::isTextElement(const QString &name){
return false;
}
bool CNode::writeNode(QXmlStreamWriter& writer,const QString& nsUri){
return true;
}
//----------------------------------------------------------------------
// запись необязательных реквизитов ЭС
//----------------------------------------------------------------------
void CNode::writeAttribute(QXmlStreamWriter& writer,const QString& name, const QString& value){
if(!value.isEmpty())
writer.writeAttribute(name, value);
}
void CNode::writeTextElement(QXmlStreamWriter& writer,const QString& nsUri,const QString& name,const QString& text){
if(!text.isEmpty())
writer.writeTextElement(nsUri,name,text);
}
//----------------------------------------------------------------------
// чтение из XML (при совпадении типов документа и объекта)
//----------------------------------------------------------------------
bool CNode::readDocument(const QString &fileName){
QFile device(fileName);
return readFromDevice(&device);
}
bool CNode::readDocument(QByteArray* array){
QBuffer device(array);
return readFromDevice(&device);
}
bool CNode::readFromDevice(QIODevice* device){
if(!device->open(QIODevice::ReadOnly | QIODevice::Text))
return false;
QXmlInputSource xmlInputSource(device);
CSaxHandler handler(this);
QXmlSimpleReader reader;
reader.setContentHandler(&handler);
bool ok=reader.parse(xmlInputSource);
device->close();
return true;
}
//----------------------------------------------------------------------
// запись в XML
//----------------------------------------------------------------------
bool CNode::writeDocument(const QString &fileName){
QFile device(fileName);
return writeToDevice(&device);
}
bool CNode::writeDocument(QByteArray* array){
array->clear();
QBuffer device(array);
return writeToDevice(&device);
}
bool CNode::writeToDevice(QIODevice* device){
QXmlStreamWriter writer(device);
if(!device->open(QIODevice::WriteOnly))
return false;
writer.setAutoFormatting(autoFormatting);
// формирование xml-документа
writer.setCodec(encoding.toAscii().data());
writer.writeStartDocument();
if(!nodeNamespace.isEmpty())
writer.writeNamespace(nodeNamespace, nodePrefix);
// вызов виртуального метода
writeNode(writer,nodeNamespace);
writer.writeEndDocument();
device->close();
return true;
}
В качестве SAX-анализатора в приведенном коде используется класс Qt QXmlSimpleReader. Для его работы нужны обработчики, которые реализованы в виде класса CSaxHandler и помещены в отдельный модуль. Для записи документа используется, как уже упоминалось, класс Qt QXmlStreamWriter.
Для методов, обеспечивающих чтение и запись XML-документов, необходимо дать некоторые пояснения.
Во-первых, понятно, что метод чтения readDocument() вызывается для уже созданного объекта конкретного типа, и исходный XML-документ должен соответствовать этому типу. Поэтому, в общем случае при чтении не известного заранее документа необходимо сначала определить его тип по имени корневого элемента и создать нужный объект. Это несложно, а то, как это сделать – смотрите в библиотеке QLibUfebs по приведенному выше адресу. Здесь же этот случай не рассматривается.
Что касается записи XML-документа, то в нашем случае для записи атрибутов и текстовых элементов в методах прикладного класса используются, соответственно, методы QXmlStreamWriter::writeAttribute() и QXmlStreamWriter::writeTextElement(). Чтобы облегчить реализацию записи необязательных реквизитов, предусмотрены методы CNode::writeAttribute() и CNode::writeTextElement() с очень похожим синтаксисом, которые формируют атрибут или элемент только для непустых значений.
продолжение - в следующем сообщении.