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. |