Abstract

This document contains the basic agreement used to implement a Data Model in Dime.

ATTENTION: Though you can in general rely on the interface specification now, minor details could be changed (and probably will). Thus apply good documenting on your code, so that you can understand it yet some days later. ;-)

TODOs:

Version History

Version Date Author Nick Changes
0.1 2002-02-03 Tim Enderling Tim Initial version.
0.3 2002-02-09 Tim Enderling Tim Reviewed version after discussion with zzorn on #dime.
0.4 2002-04-04 Tim Enderling Tim Last? review before actual implementation
0.9 2002-06-02 Tim Enderling Tim Review after implementation, changed it to a more useful doc, I hope ;-)

General idea of a Data Model

A Data Model like used in Dime is the homogenous collection of most if not all available data of the application. This is considered as very useful in connection with scripting and debugging or - in general - to make data more accessible and managable. I.e. it allows us to support different games or parts of a game, that expose different kinds of interfaces, without having to hard code a user interface for the different new features and recompile. This allows much more dynamic control of the avatar and game. E.g. when controlling a car or airship, custom UIs can be shown.

Of course, this approach has been taken by many developers, before: Take e.g. the proc filesystem of Linux or the IDispatch interface of the Component Object Model especially used in M$ VB, the Windows registry and many more... Thus it is not necessary to convince anybody how many advantages can grow of such a framework.

Nevertheless (and to give newbies some starting point...) I will list some of the flashing features of DDM (short for dime's data model):

Terms used in this document (and in the code, of course)

<Terms from graph theory> ... see the dusty maths books in your shelf, if you really don't know.

(Dime's) Data Model - DDM
(pronounce: dim)

... you know, the whole ugly thing (see above for more detailed info) - sometimes also the tree.
Data Object ... a single node of DDM
Data Model Provider - DMP (pronounce: dimp) ... class implementing the DataProvider interface (DataModelProvider was to long and abbreviations in code are not that cool) and thus hosting one branch of DDM.
Observer ... some code that registered to be called back when some event occurs. See also libsigc++ for what observers can be.

Data Model Service - DMS (pronounce: dims)

... one of the many services of dime. This one is holding the root of the Data Model.
Data Model User ... anything that calls DataObject's interface functions.
Classes starting with P ... smart pointers (usings boost's implementation) to the classes appearing when removing the front P.

Internal organization of the Data Model

Being supported by many different classes, also called DMPs, the homogenous tree structure of DDM has to be puzzled together someway, which is done 'magically' by DataObject class. Looking a bit more thorough, you'll see that each Data Object has it's specific provider, or more precisely each provider hosts one branch of the tree, following paths from it's root node to arbitrary direct and indirect childs.

Within one DMP special Data Keys are used to uniquely identify a node. This Data Key is a kind of cockie of arbitrary class (but must be derived from DataKey class). It is used instead of the subpath of a child node passing a request to the DMP. Due to the use of OOP tricks and smart pointers it is automatically destroyed if not needed anymore.

The root of the whole tree is hold by a FolderProvider, which is instantiated by the DMS. This one, like any DMP could do, adopts other DMP's root nodes. I.e. it gives some pointer to the responsible adopted DMP when asked for the child node representing it. It has not to implement any part of the interface of this node (value setting/getting, child managing, etc.), but only to hold a pointer to it. The rest, thus to find out responsible provider for a given path and use it's interface all the time, is left to the DataObject class. Data Keys that are only used to identify the root nodes of adopted DMPs are also called Adoption Keys.

In constrast to adopting a DMP the LinkProvider mimics it's desination. The differences look trivial but make important points for implementing this two kinds of DMP connection: While you can only adopt the root node of a DMP and each DMP can only be adopted once, you can mimic any node and arbitrarily often. You can be really lucky that there's LinkProvider managing this for you.

The handling of observers is implemented in the DataProvider, the general base class of all DMPs. It also passes the events back to the parent DMP to support listening to child nodes. The only thing the derived DMP has to do, is to fire the signals through the DataProvider interface, when events occure, giving the subpath of the affected node as a parameter.

Events and Signals in the Data Model

The terms event and signal are synonymously used within this document and denote actions that change of any detail, i.e. structure/value, of the Data Model. Observing of events is done by libsigc++. There are two possibilities to observe such events:

Possible events are:

C++-identifier This event is fired ...
PRE_VALUE_CHANGE ... before the value is changed. (So Data Model Users have the possibility to retrieve the old value.)
POST_VALUE_CHANGE ... after the value has changed.
POST_CHILD_ADDITION ... after a direct child, which can also be a link, was added.
PRE_CHILD_DELETION ... before a direct child object, which also can be a link, is removed.
PRE_DELETION ... before a Data Object will be deleted. This should be really implemented by every DMP.

Since not every observer wants to handle all types of events, you can OR them together to get a combination of interesting flags. Futhermore not all DMPs can support all types of events. Which events are fired for a specific Data Object is included in the type information available for all Data Objects.

Type information

The type information of a Data Object is a combination of the following flags (I don't think I have to mention that some flags exclude others... Oops, so I've done it though.)

C++-Identifier If the flag is set, ...
(event types, see above) ... the Data Model Provider fires this type of event.
VOID (is pointer to NULL), FOLDER, LIST, STREAM, BOOLEAN, INTEGER, FLOAT, STRING, CUSTOM_TYPE ... a Data Object is of a certain type. This information is not - as one can assume - necessary to use the Data Object. Instead all modes of usage are described separately below. But for information (as for debugging purposes) it should be really useful and it is needed for the framework. Custom Type means, that none of the predefined types fits so well and the conversion routines are implemented by the DMP itself, not by DataObject class.
HAS_CHILDS ... the Data Object can have child nodes in general.
ADOPTS_CHILDS ... the Data Object can have child nodes owned by other Data Model Providers. This also means that you can use links as child nodes.
CAN_SET_VALUE ... Data Model Users can change it's values.
CAN_ADD_CHILDS ... Data Model Users can add childs.
CAN_REMOVE_CHILDS ... Data Model Users can remove childs.
IS_LINK ... the Data Object is actually a link. Though it is not necessary - due to the implementational approach of links - to know when you treat a link and when an actual data object, this is considered as useful for debugging purposes.

DMPs should always be polite in cases of invalid calls to their interface functions by at least ignoring them. The only exception of this is, that a Data Model Provider can be sure that the getting and setting functions are only called for the correct type.

How to ...

... get a DataObject by it's path

    PDataObject obj = DataObject::getRoot("/The/Absolute/Path");

... test on a certain type

    if (obj->getType() & INTEGER) 
    {
        //We now that this is an int!	
    }
    
    if (obj->getType() & VOID)
    {
	//This object has been already removed actually.
    }
    

... get the value of a Data Object

    std::string strVal = obj->getStringValue(); //get the value in a printable version
    int         nVal   = obj->getIntValue(); //get the same value but converted to int

... set it's value

    try 
    {
       obj->setStringValue("none");
       obj->setIntValue(275); //set the same value but converted from int
    }
    catch (Exception & e)
    {
        //throws expections if CAN_SET_VALUE is not set for the DataObject
    }

... compare two Data Objects

    //Comparing the pointers wouldn't work! Use the paths instead. 
    //This will not work for links though.
    bool equal = (obj1->getPath() == obj2->getPath());

... add a child

    try
    {
        if (parent->getType() & ADOPTS_CHILDS)
        {
            //Could add a child handled by another DMP.
            //Will throw an exception if something is wrong with the DMP.
            
            PDataObject child = parent->addChild("AdoptedChildID", new IntProvider(25, 
                    "Some child containing the int 25"));                       
            PDataObject link  = parent->addChild("AdoptedChildID2", "/Destination/Of/Link");
        }
        
  
        if (child->getType() & CUSTOM_TYPE)
        {
            //Can set the type by the first setting call.
            
            if (childIsURL)
            {
                //E.g. this could say that the added child should be a URL.
                child->setStringValue("http://www.somedomain.org/picture.png");
            }
            else
            {
                //This can mean that a certain system picture should be used.
                child->setIntValue(5);
            }
        }  
       
        //No idea which  ID to choose? No problem, just use "" as a placeholder.                                                           
        PDataObject child2 = parent->addChild("");                
   }
   catch (Exception & e)
   {
        //Calls to addChild can lead to exceptions in general, when the DataObject doesn't
        //support adding child node (missing CAN_ADD_CHILDS).
   }

... listen to a DataObject


void MyClass::listening(PDataObject subject, DataType event)
{
    //handle the events here   
}

[...]

DataConnection connection = obj->addConnection(SigC::slot(*this, MyClass::listening), 
                            static_cast<DataType>(PRE_DELETION | POST_VALUE_CHANGE));                            
//Call this if you want to remove the connection.
connection.disconnect();

Missing something of importance here? Feel free to ask Tim Enderling or to add it here even.

Making your own DMP

You're going to make your own DMP? I just want to briefly provide some basic hints in a kind of battleplan. This cannot spare you from reading some other parts of this document.

  1. Look around you if you really need a new DMP. There are many predefined DMPs already fitting for the most common tasks.
  2. If you've really decided to do your own DMP, you should now think of it's function. Keep in mind that if you do your job well, others could use it, too. Does your DMP support child nodes? Does your DMP adopt other DMPs? Is there a certain structure in your branch?
  3. Having this, you should work on your DataKey-based type at first. There's a special template class DataKeyImpl<class T> that helps you to do this. If you have a structure called Item, that holds all necessary data to uniquely identify one of your child nodes, your Data Key is DataKeyImpl<Item>.
  4. Use DataKeyImpl<Item>::spec(PDataKey key) to get a reference to your struct and DataKeyImpl<Item>::gen(Item & item) to get a PDataKey from your struct. Remember that NULL is used as Data Key for the root node and since smartpointers are used, you have to test on (key.get() == NULL). You can by the way rely on the convention, that Data Keys of removed nodes that support PRE_DELETION-signal are never used. So it should be one of your most private goals to provide the PRE_DELETION signal.
  5. The first function you're going to implement is PDataKey getChild(PDataKey parent, std::string ID, DataProvider *& provider). If you don't support child nodes, just return PDataKey(NULL), which means that there's no child with specified ID. Otherwise you have to return a Data Key for each given parent and it's ID. If the specified child is an adopted DMP return the AdoptionKey and set the provider to the adopted one.
  6. Another function working in conjunction with getChild is void getChilds(PDataKey parent, std::vector & listOfChilds). It should fill a list with all IDs of child nodes of a certain parent. To make it possible to convert Data Keys back to subpaths the function std::string getSubpath(PDataKey key) has to be implemented.
  7. The second most important function is DataType getType(PDataKey key). Here you should return the type, the permission and feature flags and the fired events or'd together. Support as many events as possible. And don't forget to fire them at the right places, otherwise ugly bugs may occur. If the actual data is not hold by your DMP but by another class, this class should also contain also calls to fireSignal.
  8. The third very important bundle of functions is the group of getters and setters - a pair of them for each of the four primitive types. You've to implement only those one to handle a specific node, that are part of the type returned by getType on this node. If you use CUSTOM_TYPE no automatic type conversion is performed and you have to implement all eight functions. Though you may think that it's done automatically, it is not: You have to fire the changing signals if you want to support them.
  9. At last but not least, your DMP can provide the possibility to add and remove child nodes (or one of both). This first one is done by implementing void addChild(PDataKey parent, std::string & suggestedID, DataProvider * provider = NULL). The only thing to mention here is that your DMP has not to use the suggested child ID, but in any case it has to set the correct ID to suggestedID at least.
  10. As you can see from the DataProvider.h there are two removal functions: void remove(PDataKey key) and void removeAdopted(PDataKey adopted). The first one is called, whenever a Data Model User removes a key. You should remove the node if it's an Adoption Key, but instead just delete the adopted DMP. Namely, whenever an adopted DMP is deleted, it calls removeAdopted-function of it's parent. This is necessary because an adopted DMP can also be deleted for other reasons than being removed by a Data Model User.