Logo Море(!) аналитической информации!
IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware
2010 г.

Объектное представление XML-документов

sergek, март 2010

Введение

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

Подход был разработан при реализации библиотеки классов C++ для работы с XML-документами специализированных форматов. Библиотека была предназначена для проектов 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() с очень похожим синтаксисом, которые формируют атрибут или элемент только для непустых значений.

CSaxHandler – класс обработчиков SAX-анализатора

Пока мы будем использовать только три обработчика SAX-анализатора – обработчики начала и конца элементов, а также обработчик символьных данных. Это минимум, который необходим для разбора документа. Дополнительно могут потребоваться обработчики ошибок fatalError() и команд обработки processingInstruction(). Последний, в частности, может использоваться для определения кодировки документа, задаваемый в декларации XML атрибутом encoding.

Класс CSaxHandler порожден от класса Qt QxmlDefaultHandler, содержащего весь необходимый набор обработчиков парсера, которые по умолчанию ничего не делают. Для того чтобы расширить функциональность нашего класса, достаточно добавить в него объявление и реализацию соответствующих методов. Очень удобно.

// csaxhandler.h

#ifndef CSAXHANDLER_H
#define CSAXHANDLER_H

#include <QXmlDefaultHandler>
#include <QStack>

//----------------------------------------------------------------------
// обработчики для SAX-парсера
//----------------------------------------------------------------------

class CNode;

class CSaxHandler : public QXmlDefaultHandler
{
private:
    CNode* doc;               // указатель на объект
    QStack<CNode*> nodeStack; // стек обрабатываемых элементов
    QString textElement;      // буфер содержимого текстового элемента
    QString encoding;         // кодировка документа
public:
    CSaxHandler();
    CSaxHandler(CNode* node);
    virtual ~CSaxHandler();

    // связывание объекта с обработчиками
    void setDocument(CNode* node);
    void reset();	             // очистить стек и буферы

    // обработчики
    bool startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &attributes);
    bool characters(const QString &str);
    bool endElement(const QString &namespaceURI, const QString &localName, const QString &qName);
};
//----------------------------------------------------------------------

#endif // CSAXHANDLER_H

Объект, с которым взаимодействует SAX-анализатор при разборе XML-документа, передается в обработчики в виде указателя doc. Это выполняется либо в конструкторе, либо в явном виде методом setDocument().

В определении класса (ниже) видно, что этот указатель помещается в стек nodeStack. В дальнейшем, по мере продвижения по содержимому документа, в этот стек помещаются и удаляются указатели на узлы объекта. Это обеспечивает работу с вложенными объектами узловых классов синхронно с разбором документа.

// csaxhandler.cpp

#include "csaxhandler.h"
#include "cnode.h"

//----------------------------------------------------------------------

CSaxHandler::CSaxHandler(){
    reset();
}

CSaxHandler::CSaxHandler(CNode* node){
    setDocument(node);
}

CSaxHandler::~CSaxHandler(){
    // doc не удаляем (владелец - внешняя программа)!
    textElement.clear();
    nodeStack.clear();
}

void CSaxHandler::reset(){
    doc=0;
    textElement.clear();
    nodeStack.clear();
}

void CSaxHandler::setDocument(CNode* node){
    reset();
    doc=node;

    // корневой элемент
    nodeStack.push(doc);
}
//----------------------------------------------------------------------

bool CSaxHandler::startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &attributes){

    if(nodeStack.isEmpty())
        return false;

    // текущий элемент
    CNode* node=nodeStack.top();

    // обрабатываемый элемент
    if(node)
        node=node->getNode(localName);

    // инициализация реквизитов
    if(node)
        node->setRequisites(localName,attributes);

    // сделаем его текущим
    nodeStack.push(node);
    textElement.clear();
    return true;
}
//----------------------------------------------------------------------

bool CSaxHandler::characters(const QString &str){
    textElement+=str;
    return true;
}
//----------------------------------------------------------------------

bool CSaxHandler::endElement(const QString &namespaceURI, const QString &localName, const QString &qName){
    if(nodeStack.isEmpty())
        return false;

    CNode* node=nodeStack.top();

    // инициализация текстовых элементов
    if(node && node->isTextElement(localName)){
        QXmlAttributes textAttr;
        textAttr.append(localName,"","",textElement);
        node->setRequisites(localName,textAttr);
    }

    // элемент обработан
    nodeStack.pop();
    return true;
}
//----------------------------------------------------------------------

Реквизиты объекта, соответствующие атрибутам исходного документа, инициализируются в обработчике startElement(), реквизиты, соответствующие символьным данным, – в endElement(). Для инициализации используется один и тот же метод интерфейсного класса setRequisites(). Для этого значение текстового элемента записывается в объект класса QXmlAttributes, используемого для передачи атрибутов.

Это искусственный прием, позволяющий сэкономить один метод в интерфейсе CNode. Правда, при этом немного усложняется реализация setRequisites() в узловых классах, поскольку в нем появляется дополнительный условный оператор. Альтернатива – добавление в интерфейс метода инициализации только текстовых реквизитов. Что лучше – судите сами. Автору представляется, что его вариант более экономный.

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

Использование объектного представления

Исходный XML-документ

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

<?xml version="1.0" encoding="WINDOWS-1251"?>
<ED201 xmlns="urn:cbr-ru:ed:v2.0" CtrlCode="0999" CtrlTime="10:13:37" EDNo="805253" EDDate="2010-03-24" EDAuthor="4552000000">
	<Annotation>Ошибка при обработке ЭС</Annotation>
	<EDRefID EDNo="1000" EDDate="2010-03-24" EDAuthor="4525545000"/>
</ED201>
Пример иерархии прикладных классов

На рисунке представлена статическая UML-диаграмма класса, являющегося объектным представлением нашего XML-документа:

Намеренно выбран пример, где один из узловых классов (CEDRefID), объект которого включен в качестве члена класса CED201, используется также и как предок этого класса. Такие структурные решения являются обычным делом в объектном проектировании, и позволяют значительно сэкономить затраты за счет повторного использования кода. И, как можно будет убедиться далее, это оказывает влияние на метод записи данных при формировании XML-документа.

Объявления узловых классов

Узловые (прикладные) классы конструируются очень просто:

  1. Порождаем их от CNode.
  2. В защищенной части (protected) класса объявляем четыре виртуальных интерфейсных метода. Для классов в конце иерархии наследования их можно объявлять и в закрытой области (private). Есть только особенность, касающаяся метода writeNode() – он вызывается для объектов, являющихся членами других объектов (в документе это – вложенные элементы). В таких случаях есть выбор – либо прятать этот метод и объявлять друзей класса, либо объявлять его в открытой области;
  3. В открытой части объявляем конструктор по умолчанию и реквизиты с именами, совпадающими с именами атрибутов или текстовых элементов. Вложенные элементы объявляются как члены в виде объектов других узловых классов.

Часто бывает, что нет необходимости объявлять некоторые из методов. Например, в CEDRefID нет текстовых элементов, вложенных объектов, поэтому отсутствуют isTextElement() и getNode():

// cbr_ed201.h

#ifndef   cbr_ed201H
#define   cbr_ed201H

#include "cnode.h"

//----------------------------------------------------------------------

// EDRefID

class CEDRefID : public CNode
{
protected:
    virtual void setRequisites(const QString &name,const QXmlAttributes &attributes);
public:
    virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);
    CEDRefID();

    // Methods & Properties
    QString EDNo;
    QString EDDate;
    QString EDAuthor;
};
//----------------------------------------------------------------------

// ED201 

class CED201 : public CEDRefID
{
private:
    virtual void setRequisites(const QString &name,const QXmlAttributes &attributes);
    virtual CNode* getNode(const QString &name);
    virtual bool isTextElement(const QString &name);
    virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);
public:
    CED201();

    // Methods & Properties
    QString CtrlCode;
    QString CtrlTime;
    QString Annotation;
    CEDRefID EDRefID;
};
//----------------------------------------------------------------------

#endif

Имена реквизитов в классах объявлены с нарушением принятого в C++ стиля именования (с прописной буквы). Это не небрежность автора. Дело в том, что в описании форматов XML-документов, для которых реализованы эти классы, принята именно такая нотация. А в объявление класса они попали методом «copy/paste». И вообще, весь подход объектного представления направлен на то, чтобы процесс конструирования сводился к простым формальным приемам.

Реализация классов

Для наглядности в данном подразделе текст модуля cbr_ed201.cpp разделен на части, с комментариями перед каждой его частью.

В конструкторе узлового класса CEDRefID задаются пространство имен nodeNamespace и его префикс nodePrefix. Это не обязательно. Можно опустить либо оба присвоения (тогда действует ранее объявленное или пространство имен по-умолчанию), либо опустить префикс. Если не задавать префикс, тогда он будет формироваться в соответствии с областью действия пространства имен в форме «n1», «n2» и т.д.:

// cbr_ed201.cpp

#include "cbr_ed201.h"
#include <QXmlAttributes>
#include <QXmlStreamWriter>
//----------------------------------------------------------------------

// EDRefID
CEDRefID::CEDRefID(){
    // пространство имен
    nodeNamespace = "urn:cbr-ru:ed:v2.0";
    nodePrefix    = "ed";
}

Так выполняется присвоение реквизитов объекта, являющихся аналогом атрибутов XML-документа (для текстовых элементов будет показано ниже):

// инициализация реквизитов документа при чтении ЭД
void CEDRefID::setRequisites(const QString &,const QXmlAttributes &attributes){
    EDNo=attributes.value("EDNo");
    EDDate=attributes.value("EDDate");
    EDAuthor=attributes.value("EDAuthor");
}

Поскольку EDRefID является элементом исходного документа (узлом), для него определен метод writeNode(), начинающийся с записи открывающего тега writeStartElement() и заканчивающийся записью закрывающего тега writeEndElement():

bool CEDRefID::writeNode(QXmlStreamWriter& writer,const QString& nsUri){
    writer.writeStartElement(nsUri,nodeName);
    writer.writeAttribute("EDNo", EDNo);
    writer.writeAttribute("EDDate", EDDate);
    writer.writeAttribute("EDAuthor", EDAuthor);
    writer.writeEndElement();
    return true;
}

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

// ED201
CED201::CED201(){
    nodeName="ED201";
    EDRefID.nodeName="EDRefID";
}

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

Инициализацию реквизитов класса-родителя CEDRefID можно выполнить либо явным образом, как и остальные реквизиты (что может привести к проблемам при изменении формата документа), либо вызовом метода с явным разыменованием (предпочтительно):

// инициализация реквизитов документа при чтении ЭД
void CED201::setRequisites(const QString &name,const QXmlAttributes &attributes){
    if(name=="Annotation")
        Annotation=attributes.value(name);
    else{
        // инициализация реквизитов базового класса
        CEDRefID::setRequisites(name,attributes);

        CtrlCode=attributes.value("CtrlCode");
        CtrlTime=attributes.value("CtrlTime");
    }
}

Этот метод должен быть определен в двух случаях – если класс содержит вложенные объекты (в нашем случае – EDRefID), либо если в классе есть реквизиты, являющиеся аналогом текстовых элементов (Annotation):

CNode* CED201::getNode(const QString &name){
    if(name==nodeName || name=="Annotation")
        return this;
    else if(name=="EDRefID")
        return &EDRefID;
    else
        return 0;
}

Для класса, содержащего реквизиты – аналог текстовых элементов, нужно определить этот метод:

bool CED201::isTextElement(const QString &name){
    return (name=="Annotation");
}

В данном примере есть небольшая особенность. Реквизиты EDNo, EDDate, EDAuthor наследуются от класса СEDRefID, но использовать метод СEDRefID::writeNode() мы не можем, т.к. в этом случае сформируются открывающий и закрывающий теги элемента. Поэтому запись этих реквизитов выполняется так, как если бы они были объявлены в CED201:

bool CED201::writeNode(QXmlStreamWriter& writer,const QString& nsUri){
    writer.writeStartElement(nsUri,nodeName);
    writer.writeAttribute("EDNo", EDNo);
    writer.writeAttribute("EDDate", EDDate);
    writer.writeAttribute("EDAuthor", EDAuthor);
    writer.writeAttribute("CtrlCode", CtrlCode);
    writer.writeAttribute("CtrlTime", CtrlTime);
    writer.writeTextElement(nsUri,"Annotation", Annotation);
    EDRefID.writeNode(writer,nsUri);
    writer.writeEndElement();
    return true;
}

В заключение несколько слов о записи необязательных реквизитов. Если какой-либо атрибут может отсутствовать в XML-документе, то его запись нужно выполнять, используя альтернативные методы интерфейсного класса CNode::writeAttribute(),CNode::writeTextElement(). Например, запись

    writer.writeAttribute("EDNo", EDNo);

надо заменить на следующую:

    writeAttribute(writer, "EDNo", EDNo);
Использование в прикладной программе

Здесь приведен пример использования сконструированных классов в прикладной программе.

Входной документ:

<?xml version="1.0" encoding="WINDOWS-1251"?>
<ED201 xmlns="urn:cbr-ru:ed:v2.0" CtrlCode="0999" CtrlTime="10:13:37" EDNo="805253" EDDate="2010-03-24" EDAuthor="4552000000">
	<Annotation>Ошибка при обработке ЭС</Annotation>
	<EDRefID EDNo="1000" EDDate="2010-03-24" EDAuthor="4525545000"/>
</ED201>

Слот xmlSlot() выполняет чтение XML-документа text, содержащегося в текстовом редакторе textEdit, в объект ed. Затем с использованием этого объекта выполняется изменение реквизитов и запись объекта в выходной XML-документ out, который добавляется в текстовый редактор для отображения на экране:

void MainWindow::xmlSlot(){
    QByteArray in;
    QString text=textEdit->toPlainText();
    in.append(text);

    // 1. чтение XML-документа
    CED201 ed;
    ed.readDocument(&in);

    // 2. работа с реквизитами
    ed.EDNo = "1";
    ed.EDDate = "2010-03-01";
    ed.EDAuthor = "4552000001";

    // 3. запись XML-документа
    QByteArray out;
    ed.writeDocument(&out);

    textEdit->append("");
    textEdit->append(out);
}

В результате получаем XML-документ:

<?xml version="1.0" encoding="windows-1251"?>
<ed:ED201 xmlns:ed="urn:cbr-ru:ed:v2.0" EDNo="1" EDDate="2010-03-01" EDAuthor="4552000001" CtrlCode="0999" CtrlTime="10:13:37">
	<Annotation>Ошибка при обработке ЭС</Annotation>
	<EDRefID EDNo="1000" EDDate="2010-03-24" EDAuthor="4525545000"/>
</ed:ED201>
Пример для документа с повторяющимися элементами

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

<?xml version="1.0" encoding="WINDOWS-1251"?>
<ED232 xmlns="urn:cbr-ru:ed:v2.0" EDAuthor="4525000000" EDDate="2008-03-14" EDNo="1005">
	<PLAN BS="10101" RKC="2" Type="2"/>
	<PLAN BS="10207" RKC="1" Type="2"/>
	<PLAN BS="10208" RKC="1" Type="2"/>
</ED232>

Объявление класса для этого документа может выглядеть так (опускаем объявление класса CPLAN):

class CPLAN;
typedef QVector<CPLAN *> CPLANList;

class CED232 : public CED
{
private:
    virtual CNode* getNode(const QString &name);
    virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);
public:
    CED232();
    ~CED232();

    CEDRefID InitialED;
    CPLANList PLAN;
};

Как видно, повторяющаяся часть документа реализована в виде списка с использованием шаблона QVector, аналогичного вектору стандартной библиотеки. В список содержатся указатели на объекты, созданные в памяти. Поэтому для класса CED232 нужен деструктор, который освобождает память, занятую объектами CPLAN:

CED232::~CED232(){
    for(int i=0; i<PLAN.size(); i++)
        delete PLAN[i];
}

Методы класса можно реализовать так:

CNode* CED232::getNode(const QString &name){
    if(name==nodeName)
        return this;
    else if(name=="PLAN"){
        CPLAN* info=new CPLAN();
        PLAN.push_back(info);
        return info;
    }else
        return 0;
}
//----------------------------------------------------------------------

bool CED232::writeNode(QXmlStreamWriter& writer,const QString& nsUri){
    writer.writeStartElement(nsUri,nodeName);
    writer.writeAttribute("EDNo", EDNo);
    writer.writeAttribute("EDDate", EDDate);
    writer.writeAttribute("EDAuthor", EDAuthor);

    for(int i=0; i<PLAN.size(); i++)
        PLAN[i]->writeNode(writer,nsUri);

    writer.writeEndElement();
    return true;
}

Заключение

Кому-то может показаться, что объем кода, который нужно определить при использовании предложенного объектного представления XML-документа, больше, чем хотелось бы. Однако это не так. Например, для того чтобы использовать так называемые «свойства» классов (property, расширение C++Builder), в реализации аналогичной библиотеки LibUfebs с использованием DOM приходится определять довольно много кода. К примеру, определение класса CED101 в упомянутой библиотеке занимает около 300 строк, когда как при использовании предлагаемого подхода – всего 120. И это притом, что в DOM не надо заботиться о записи XML-документов в файл.

Правда, справедливости ради надо отметить, что большая часть кода в C++Builder генерируется автоматически по XSD-схемам специальным инструментом XML Data Binding wizard. Но и ручной работы после этого остается достаточно.

Скачать пример (RAR, 10 KB)

Список использованной литературы

  1. Мартин Д., Бирбек М., Кэй М. и др. XML для профессионалов. – М.: Лори, 2001. – 900 с.
  2. Qt 4.6.2 Reference Documentation. Copyright © 2010 Nokia Corporation and/or its subsidiary(-ies).

Новости мира IT:

Архив новостей

Последние комментарии:

Loading

IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware

Информация для рекламодателей PR-акции, размещение рекламы — adv@citforum.ru,
тел. +7 985 1945361
Пресс-релизы — pr@citforum.ru
Обратная связь
Информация для авторов
Rambler's Top100 TopList liveinternet.ru: показано число просмотров за 24 часа, посетителей за 24 часа и за сегодня This Web server launched on February 24, 1997
Copyright © 1997-2000 CIT, © 2001-2015 CIT Forum
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Подробнее...