Writing Data Objects

< Binder Data Model | Storage Kit | fini >

When implementing your own data model object, you should make use of the standard C++ base classes that are available. These classes are provided for two main reasons:

There are a wide variety of classes available, so it can be daunting to figure out where to start. In general, they are designed as layers of increasingly higher-level functionality for the three core data model interfaces, INode, IIterable, and IDatum.

This document will start with the highest-level classes, and work down to the simpler ones. This should allow you to go down through the available classes until you find one that will do what you want, and stop there. Remember that you should always use one of these classes to implement the INode, IIterable, or IDatum interfaces; as a very last resort, that will be BGenericNode, BGenericIterable, and BGenericDatum, respectively.

  1. Design Overview
  2. Implementation Helper Classes
    1. Delegation
    2. DataManager
    3. Tables
    4. Arrays
    5. Nodes
    6. Iterables
    7. Datums
  3. Example

Design Overview

Before looking at the classes in detail, there are some general concepts behind their design that should be understood.

In general, these classes provide an implementation of the Binder Data Model interfaces on top of some other data structure that you will provide. Providing the data structure means implementing a subclass of the desired data model class and filling in the appropriate virtuals to give it access to your data.

The implementation of the data model classes and the underlying data they access must be, however, closely related. In particular, there are many cases where the implementation will need to do a series of operations during which it is assured that the back-end data remains in a consistent state. Because of this requirement, the locking approach used for these classes is very different than what you will see elsewhere.

Almost all of the data model classes explicitly expose their internal locking, so that it can be synchronized with the back-end data. This takes the form of a couple public virtuals to explicitly lock and unlock the object, and a convention for users to know when that lock is being held.

Note:
As mentioned, this locking policy is very different than what is used elsewhere in the Binder Kit. You should generally prefer the Binder's standard locking model (as described in Threading Conventions), since that is more flexible and results in a much easier to use API.
By convention, all methods in these classes that are called with the object's lock held (or put another way, must be called while holding that lock) have the suffix "Locked" in their name. A new method defined by one of these classes that does not have the "Locked" suffix must be called without holding the lock (since it may need to acquire the lock in its implementation).

In some cases, a method without the "Locked" suffix may be safe to call with or without the lock held; this situation will be documented. It is also, of course, safe to call the SAtom, BBinder, and other standard Binder APIs with or without the lock being held, since these use the standard Threading Conventions model.

For example, compare BGenericNode::SetMimeType() to BGenericNode::SetMimeTypeLocked().

The Lock() methods return a lock_status_t result, making them convenient to use with the SAutolock class, like so:

void set_catalog_mime_type(const sptr<BCatalog>& catalog)
{
    SAutolock _l(catalog->Lock());
    catalog->StoreMimeTypeLocked(mime_type);
}

When mixing two classes together that have their own locks, you will usually need to re-implement their Lock() and Unlock() methods so that they share the same lock:

class NodeAndIterable : public BMetaDataNode, public BIndexedIterable
{
public:
    virtual lock_status_t Lock() const { return BMetaDataNode::Lock(); }
    virtual void Unlock() const        { return BMetaDataNode::Unlock(); }
};

This goes along with the standard mixing of the IBinder::Inspect() method that is done when mixing together multiple interfaces.

Implementation Helper Classes

This section gives an overview and summary of the available helper classes for implementing data objects, going from highest level to lowest.

Delegation

These classes allow you to wrap an existing data model object, providing an implementation that modifies its behavior. For example, you could use them to rename all of the entries in a node from X to foo-X.

Tables

A common data structure is that of a table: at the top an array of rows, each of which itself contains an array of columns. This is used, for example, to provide data to a list view.

Arrays

An array is the simplest data structure available, an ordered series of values. More complicated data structures are created arrays by nesting arrays inside of each other, such as the rows inside of a table.

Nodes

These classes provide the most generic support for implementing the INode interface.

Iterables

These classes provide the most generic support for imlementing the IIterable, and thus IIterator and IRandomIterator, interfaces.

Datums

These classes provide the most generic support for implementing the IDatum interface.

Example

This example shows the implementation of a "content provider" for contact/person information. It uses BSchemaDatabaseNode to expose multiple database tables in various ways. The top-level content provider is a BStructuredNode, used to publish a fixed set of entries at that level for selection of the various kinds of data (tables) contained inside of it.

Note:
The classes used here are part of an old implementation of the data model on top of Cobalt's Data Manager APIs. They are not built as part of the current OpenBinder distribution, but are included to illustration how this kind of data can be presented in the data model.
#include <support/Package.h>
#include <storage/StructuredNode.h>
#include <storage/SchemaDatabaseNode.h>
#include <storage/SchemaRowIDJoin.h>

#include <support/Autolock.h>
#include <support/Iterator.h>
#include <support/Node.h>

#include <DataMgr.h>
#include <Loader.h>
#include <SchemaDatabases.h>

// Top-level content provider object, containing sub-directories
// to access the various tables of information.
class AddressContentProvider : public BStructuredNode, public SPackageSptr
{
public:
                                    AddressContentProvider(const SContext& context, const SValue& args);
            void                    InitAtom();

    virtual SValue                  ValueAtLocked(size_t index) const;

private:
            sptr<BSchemaDatabaseNode>   m_database;
            sptr<BSchemaTableNode>      m_people;
            sptr<BSchemaTableNode>      m_phones;
            sptr<BSchemaTableNode>      m_extras;
            sptr<BSchemaRowIDJoin>      m_phonesDir;
            sptr<BSchemaRowIDJoin>      m_extrasDir;
};

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

// Various constants we will use elsewhere.

B_STATIC_STRING_VALUE_LARGE(kPersonListMimeType, "application/vnd.palm.personlist" ,);
B_STATIC_STRING_VALUE_LARGE(kPersonItemMimeType, "application/vnd.palm.personitem" ,);
B_STATIC_STRING_VALUE_LARGE(kPhoneListMimeType, "application/vnd.palm.phonelist" ,);
B_STATIC_STRING_VALUE_LARGE(kPhoneItemMimeType, "application/vnd.palm.phoneitem" ,);
B_STATIC_STRING_VALUE_LARGE(kPersonExtrasListMimeType, "application/vnd.palm.personextraslist" ,);
B_STATIC_STRING_VALUE_LARGE(kPersonExtrasItemMimeType, "application/vnd.palm.personextrasitem" ,);
B_CONST_STRING_VALUE_LARGE(kAddressDBName, "AddressDBSNu" ,);
B_CONST_STRING_VALUE_LARGE(kAddressMainTableName, "AddressBookMain" ,);
B_CONST_STRING_VALUE_LARGE(kAddressPhoneTableName, "AddressBookPhone" ,);
B_CONST_STRING_VALUE_LARGE(kAddressExtraTableName, "AddressBookExtra" ,);

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

// This class is used to implement a custom database column
// based on the "FirstName" and "LastName" columns.

const char* displayColumns[] = { "FirstName", "LastName", NULL };

class DisplayName : public BSchemaTableNode::CustomColumn, public SPackageSptr
{
public:
    DisplayName(const sptr<BSchemaTableNode>& table)
        : CustomColumn(table)
    {
    }

    virtual SValue ValueLocked(uint32_t columnOrRowID, const SValue* baseValues,
                               const size_t* columnsToValues) const
    {
        SString result(baseValues[columnsToValues[1]].AsString());
        const SString first(baseValues[columnsToValues[0]].AsString());
        if (first != "") result += ", ";
        result += first;
        return SValue::String(result);
    }
};

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

// These are the top-level directories in the content provider.
const char* kDirNames[] = {
    "people",
    "phones",
    "extras",
    "databases",
};
enum {
    kPeopleDir = 0,
    kPhonesDir,
    kExtrasDir,
    kDatabasesDir,
    kDirCount
};

AddressContentProvider::AddressContentProvider(const SContext& context, const SValue& args)
    : BStructuredNode(context, kDirNames, kDirCount)
{
}

// Main initialization of content provider.
void AddressContentProvider::InitAtom()
{
    BStructuredNode::InitAtom();

    // Try to open the database.  If it doesn't exist, create it.
    SDatabase database(((const SString&)kAddressDBName).String(), 'add2',
        dmModeReadWrite, dbShareRead);
    if (database.StatusCheck() != errNone) {
        // Error opening database -- try to create it in case it doesn't exist.
        MemHandle   memh;
        void *      memp;
        DatabaseID  dbid;
        status_t    err = errNone;

        DmOpenRef appdb;
        SysGetModuleDatabase(SysGetRefNum(), NULL, &appdb);

        if ((memh = DmGetResource(appdb, 'scdb', SCHEMA_DEFINITION_ID)) == NULL)
            return;

        if ((memp = MemHandleLock(memh)) == NULL)
        {
            DmReleaseResource(memh);
            return;
        }

        if ((err = DmCreateDatabaseFromImage(memp, &dbid)) < errNone)
        {
            return;
        }

        MemHandleUnlock(memh);
        DmReleaseResource(memh);

        database = SDatabase(((const SString&)kAddressDBName).String(), 'add2',
            dmModeReadWrite, dbShareRead);
    }

    // Create the database node, and build up our content
    // provider's subdirectories from it.
    if (database.StatusCheck() == errNone) {

        // Create database object and retrieve its tables.
        m_database = new BSchemaDatabaseNode(Context(), database);
        m_database->Lock();
        m_people = m_database->TableForLocked(kAddressMainTableName);
        m_phones = m_database->TableForLocked(kAddressPhoneTableName);
        m_extras = m_database->TableForLocked(kAddressExtraTableName);
        m_database->Unlock();

        // Create a join between the people and phone number tables.
        if (m_people != NULL && m_phones != NULL) {
            m_phonesDir = new BSchemaRowIDJoin(Context(), m_people, m_phones,
                kPersonID, kperson);
            if (m_phonesDir != NULL) {
                SAutolock _l(m_phonesDir->Lock());
                m_phonesDir->StoreMimeTypeLocked(kPhoneListMimeType);
                m_phonesDir->StoreRowMimeTypeLocked(kPhoneItemMimeType);
            }
        }

        // Create a join between the people and extra info tables.
        if (m_people != NULL && m_extras != NULL) {
            m_extrasDir = new BSchemaRowIDJoin(Context(), m_people, m_extras,
                kPersonID, kperson);
            if (m_emailsDir != NULL) {
                SAutolock _l(m_emailsDir->Lock());
                m_extrasDir->StoreMimeTypeLocked(kPersonExtrasListMimeType);
                m_extrasDir->StoreRowMimeTypeLocked(kPersonExtrasItemMimeType);
            }
        }

        // Add a custom column to the people table and set its MIME type.
        if (m_people != NULL) {
            sptr<DisplayName> dname = new DisplayName(m_people);
            if (dname != NULL)
                dname->AttachColumn(SString("DisplayName"), displayColumns);
            SAutolock _l(m_people->Lock());
            m_people->StoreMimeTypeLocked(kPersonListMimeType);
            m_people->StoreRowMimeTypeLocked(kPersonItemMimeType);
        }
    }
}

// Provide access to our sub-directories.
SValue AddressContentProvider::ValueAtLocked(size_t index) const
{
    switch (index) {
        case kPeopleDir: return SValue::Binder((BnNode*)m_people.ptr());
        case kPhonesDir: return SValue::Binder((BnNode*)m_phonesDir.ptr());
        case kExtrasDir: return SValue::Binder((BnNode*)m_extrasDir.ptr());
        case kDatabasesDir: return SValue::Binder((BnNode*)m_database.ptr());
    }
    return SValue::Undefined();
}