Serializer Pattern Revisited

In this section, we combine QMetaObjects with the SAX2 parser to show how one can write a general-purpose XML encoding/decoding tool that works on QObjects with well-defined Q_PROPERTYs and children. This gives us a nice example that combines the MetaObject pattern with the Serializer pattern.

To encode and decode DataObjects as XML, we must define a mapping scheme. Such a mapping must capture not only the QObject's properties, types, and values, but it must also capture existing relationships between the object and its children, between each child and all of its children, and so on.

The parent-child relationships of XML elements naturally map to QObject parents and children. These relationships define a tree structure.

Consider the class definition for Customer shown in Example 16.14.

Example 16.14. src/xml/propchildren/customer.h

[ . . . . ]
class Customer : public QObject {
 Q_OBJECT
 public:
 Q_PROPERTY( QString Name READ objectName WRITE setObjectName );
 Q_PROPERTY( QDate Date READ getDate WRITE setDate );
 Q_PROPERTY( int LuckyNumber READ getLuckyNumber
 WRITE setLuckyNumber );
 Q_PROPERTY( QString State READ getState WRITE setState );
 Q_PROPERTY( QString Zip READ getZip WRITE setZip );
 Q_PROPERTY( QString FavoriteFood READ getFavoriteFood
 WRITE setFavoriteFood );
 Q_PROPERTY( QString FavoriteDrink READ getFavoriteDrink
 WRITE setFavoriteDrink);

 // typical setters and getters
[ . . . . ]
 private:
 QString m_Name, m_State, m_Zip;
 QString m_FavoriteFood, m_FavoriteDrink;
 QDate m_Date;
 int m_LuckyNumber;
};
[ . . . . ]

Exploiting the ability of QObject subclasses to maintain a collection of child objects, we define a CustomerList class in Example 16.15 that stores Customers as children.

Example 16.15. src/xml/propchildren/customerlist.h

#ifndef CUSTOMERLIST_H
#define CUSTOMERLIST_H

#include 
#include "customer.h"

class CustomerList : public QObject {
 Q_OBJECT
 public:
 CustomerList(QString listname = QString()) {
 setObjectName(listname);
 }
 QList getCustomers();
 static CustomerList* sample();
};

#endif

An example of the desired XML format for storing the data of a CustomerList is shown in Example 16.16.

Example 16.16. src/xml/propchildren/customerlist.xml


 

With this kind of information in an input file, we should be able to fully reconstruct not only the properties and their types, but also the tree structure of parent-child relationships between objects for a CustomerList.

16.2.1. Exporting to XML

We define in Example 16.17 a simplified class that can be used to export the current state of a QObject to an XML string with elements that contain, for each property, its name, type, and value.

Example 16.17. src/xml/propchildren/xmlexport.h

[ . . . . ]
class XMLExport {
 public:
 virtual ~XMLExport() {}
 virtual QString objectToXml(const QObject* ptr,
 int indentlevel=0);
};
[ . . . . ]

In Example 16.18 we show the definition of objectToXml(), a recursive function that constructs strings for each of the object's properties and then iterates over the object's children, recursively calling objectToXml() on each child.

Example 16.18. src/xml/propchildren/xmlexport.cpp

[ . . . . ]
QString XMLExport::objectToXml(const QObject* doptr,
 int indentlevel) {
 QStringList result;
 QString indentspace;

 indentspace.fill(' ', indentlevel*3);
 const QMetaObject* meta = doptr->metaObject();
 result += QString("
%1").
 arg(indentspace).
 arg(meta->className()).
 arg(doptr->objectName());

 for (int i= 0; i < meta->propertyCount(); ++i) { <-- 1
 QMetaProperty qmp = meta->property(i);
 const char* propname = qmp.name();
 if (strcmp(propname, "objectName")==0)
 continue;
 QVariant qv;
 if (qmp.isEnumType()) {
 QMetaEnum qme = qmp.enumerator();
 qv = qme.valueToKey(qv.toInt());
 } else {
 qv = doptr->property(propname);
 }

 result += QString (
 "%1 "
 ).arg(indentspace).arg(propname). arg(qv.typeName())
 .arg(variantToString(qv));
 }

 QObjectList childlist = doptr->findChildren (QString());
 foreach (QObject* objptr, childlist) { <-- 2
 if (objptr->parent()==doptr) { <-- 3
 result += objectToXml(objptr, indentlevel+1); <-- 4
 }
 }
 result += QString("%1
").arg(indentspace);
 return result.join("
");
}
[ . . . . ]
 

(1)Iterate through each property.

(2)Iterate through the child list.

(3)findChildren also includes grandchildren and great-great grandchildren, so we skip over those.

(4)recursive call

objectToXml() uses Qt's properties and QMetaObject facilities to reflect on the class. As it iterates it appends each line to a QStringList. When iteration is complete, the is closed. The return QString is then produced quickly by calling QStringList::join(" ").

16.2.2. Importing Objects with an Abstract Factory

Section 14.2

The importing routine is a bit more sophisticated than the exporting routine, and it has a couple of interesting features.

  • It parses XML using the SAX parser.
  • Depending on the input, it creates objects.
  • The number and types of objects, as well as their parent-child relationships, must be reconstructed from the information in the file.

Example 16.19 shows the class definition for DataObjectReader.

Example 16.19. src/libs/dataobjects/dataobjectreader.h

[ . . . . ]
#include 
#include 
#include 
#include 

class AbstractFactory;
class DataObject;
class DataObjectReader : public QXmlDefaultHandler {
 public:
 DataObjectReader (AbstractFactory* factory=0) :
 m_Factory(factory), m_Current(0) { }
 DataObjectReader (QString filename,
 AbstractFactory* factory=0);
 void parse(QString text);
 void parseFile(QString filename);
 DataObject* getRoot();
 ~DataObjectReader();

 // callback methods from QXmlDefaultHandler
 bool startElement( const QString & namespaceURI,
 const QString & name,
 const QString & qualifiedName,
 const QXmlAttributes & attributes );
 bool endElement( const QString & namespaceURI,
 const QString & localName,
 const QString & qualifiedName);
 bool endDocument();
 private:
 void addCurrentToQueue();
 AbstractFactory* m_Factory;
 DataObject* m_Current;
 QQueue m_ObjectList;
 QStack m_ParentStack;
};
[ . . . . ]

Figure 16.2 shows the relationships between the various classes that we will be using.

Figure 16.2. DataObjectReader and its related classes

DataObjectReader is derived from QXmlDefaultHandler, which is a plugin for the QXmlSimpleReader. AbstractFactory is a plugin for DataObjectReader. When we create a DataObjectReader, we must supply it with a concrete class, such as ObjectFactory or DataObjectFactory.

DataObjectReader is now completely separate from the specific types of objects that it can create. To use it with your own types, just derive a factory from AbstractFactory for them.

Think about Example 16.16 as you read the code that constructs objects from it in the code the follows.

startElement() is called when the SAX parser encounters the initial tag of an XML element. As we see in Example 16.20, the parameters to this function contain all the information we need to create an object. All other objects that are encountered between startElement() and the matching endElement() are children of m_Current.

Example 16.20. src/libs/dataobjects/dataobjectreader.cpp

[ . . . . ]

bool DataObjectReader::startElement( const QString &,
 const QString & elementName,
 const QString &,
 const QXmlAttributes & atts) { <-- 1
 if (elementName == "object") {
 if (m_Current != 0) <-- 2
 m_ParentStack.push(m_Current); <-- 3
 QString classname = atts.value("class");
 QString instancename = atts.value("name");
 if (m_Factory ==0) {
 m_Current =
 ObjectFactory::instance()->newObject(classname);
 } else {
 m_Current=m_Factory->newObject(classname);
 }
 m_Current->setObjectName(instancename);
 if (!m_ParentStack.empty()) { <-- 4
 m_Current->setParent(m_ParentStack.top());
 }
 return true;
 }
 if (elementName == "property") {
 QString fieldType = atts.value("type");
 QString fieldName = atts.value("name");
 QString fieldValue = atts.value("value");
 QVariant qv = variantFrom(fieldType, fieldValue);
 bool ok = m_Current->setProperty(fieldName, qv);
 if (!ok) {
 qDebug() << "setProperty(" << fieldName << ") failed";
 }
 }
 return true;
}
 

(1)Unnamed parameters are a way of avoiding "parameter not used" warnings from the compiler. It is necessary to include the parameters, even though we do not need them for this application, so that the signature matches that of the base class method and polymorphic overrides will be properly called.

(2)if we are already inside an

(3)Keep track of the current parent.

(4)If this element has a parent, it is on the top of the stack. Set its parent.

The Object is "finished" when we reach endElement(), which is defined in Example 16.21.

Example 16.21. src/libs/dataobjects/dataobjectreader.cpp

[ . . . . ]

bool DataObjectReader::endElement( const QString & ,
 const QString & elementName,
 const QString & ) {
 if (elementName == "object") {
 if (!m_ParentStack.empty())
 m_Current = m_ParentStack.pop();
 else {
 addCurrentToQueue();
 }

 }
 return true;
}

DataObjectReader uses an Abstract Factory to do the actual object creation.

The callback function, newObject(QString className), creates an object that can hold all of the properties described in className. ObjectFactory creates "pseudo-objects" that are not exactly the CustomerList and Customer classes, but they "mimic" them well enough that the export/import process works round-trip. You can write a concrete factory that returns the proper types for each classname if you want the de-serialized tree to have the same types as the objects in the original tree.

Each time a new address type is added to this library, we can add another else clause to the createObject function, as shown in Example 16.22.

Example 16.22. src/libs/dataobjects/objectfactory.cpp

[ . . . . ]

DataObject* ObjectFactory::newObject(QString className) {
 DataObject* retval = 0;
 if (className == "UsAddress") {
 retval = newAddress(Country::USA);
 } else if (className == "CanadaAddress") {
 retval = newAddress(Country::Canada);
 } else {
 qDebug() << QString("Generic PropsMap created for new %1 ").
 arg(className);
 retval = new PropsMap(className);
 retval->setParent(this); <-- 1
 }
 return retval;
}
 

(1)Initially set the parent of the new object to the factory.


Part I: Introduction to C++ and Qt 4

C++ Introduction

Classes

Introduction to Qt

Lists

Functions

Inheritance and Polymorphism

Part II: Higher-Level Programming

Libraries

Introduction to Design Patterns

QObject

Generics and Containers

Qt GUI Widgets

Concurrency

Validation and Regular Expressions

Parsing XML

Meta Objects, Properties, and Reflective Programming

More Design Patterns

Models and Views

Qt SQL Classes

Part III: C++ Language Reference

Types and Expressions

Scope and Storage Class

Statements and Control Structures

Memory Access

Chapter Summary

Inheritance in Detail

Miscellaneous Topics

Part IV: Programming Assignments

MP3 Jukebox Assignments

Part V: Appendices

MP3 Jukebox Assignments

Bibliography

MP3 Jukebox Assignments



An Introduction to Design Patterns in C++ with Qt 4
An Introduction to Design Patterns in C++ with Qt 4
ISBN: 0131879057
EAN: 2147483647
Year: 2004
Pages: 268

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net