"""4SuiteServer storage driver for MetaKit RDF back-end

Copyright 2002 Fourthought Inc, USA.
"""

"""History:
20-aug-2002 [als] created
22-aug-2002 [als] fix: SystemModel was used instead of UserModel
"""

# $RCSfile: MetaKit.py,v $
__version__ = "$Revision: 1.2 $"[11:-2]
__date__ = "$Date: 2005/02/27 04:17:32 $"[7:-2]

import os, sys

from Ft.Rdf import Model
from Ft.Rdf.Drivers import MetaKit
from Ft.Rdf.Drivers.MetaKit import dec_utf8, enc_utf8

SYSTEM_MODEL_NAME = 'system'
USER_MODEL_NAME = 'user'

STRING_VIEW = "ftss_strings[path:S,type:I,data:B]"

def InitializeModule():
    """
    Post-import hook to initialize module's runtime variables that are not
    required at import time, but will be needed before the module-level
    functions are called.
    """
    global metakit
    global DATABASE_DIR

    # Load the required database interface module
    import metakit

    from Ft import GetConfigVar
    dbdir = os.path.join(GetConfigVar('LOCALSTATEDIR'), 'FlatFile')
    dbdir = os.environ.get('FT_DATABASE_DIR', dbdir)

    # Normalize path, remove unnecessary slashes
    DATABASE_DIR = os.path.abspath(dbdir)

    # FIXME: l10n
    if not os.path.isdir(DATABASE_DIR):
        raise ValueError("Directory %r does not exist" % DATABASE_DIR)

    # Incorporate the RDF driver module
    MetaKit.InitializeModule()
    return


def Exists(properties):
    """See if this repo exists.
    
    Return value:
        1  - Everything is present
        0  - Some things are there
        -1 - Nothing is there.
    """
    _mk_db_name = properties["DbName"]
    _fname = MetaKit.CreateFileName(_mk_db_name)
    # XXX should we use os.path.isfile() instead?
    if not os.path.exists(_fname): return -1
    # since MetaKit is very tolerant about the database structure,
    # there is no actual need to see if the database file
    # contains anything at all
    # do the version check to behave rigorously
    try:
        MetaKit.CheckVersion(_mk_db_name)
        return 1
    except TypeError:
        return 0

def Initialize(properties):
    """Initialize a new copy of the repo.
    
    This is not the same as a 4ss_manager init.
    This is very raw.  The adapter will take care
    of calling our other interfaces (createContainer,
    createUser, etc) with all of the information
    about the rest of the 'init' stuff to do
    """
    _mk_db_name = properties["DbName"]
    MetaKit.CreateDb(_mk_db_name, SYSTEM_MODEL_NAME)
    MetaKit.CreateDb(_mk_db_name, USER_MODEL_NAME)

    _fname = MetaKit.CreateFileName(_mk_db_name)
    # Note: Initialization is done in single-user mode
    #   dependless of MetaKit.MODE_RW setting
    _db = metakit.storage(_fname, 1)
    _vw = _db.getas(STRING_VIEW)
    _db.commit()

def Destroy(properties,tryOnly=0):
    """Completly destroy the repo"""
    _fname = MetaKit.CreateFileName(properties["DbName"])
    if os.path.exists(_fname): os.unlink(_fname)

def Maintain(properties):
    """Perform any maintenance on the db"""
    _fname = MetaKit.CreateFileName(properties["DbName"])
    _tmp_fname = _fname + ".tmp"
    _bak_fname = _fname + ".bak"
    # open the database read-only
    _db = metakit.storage(_fname, 0)
    # serialize the database to tmp-file
    _f = open(_tmp_fname, "wb")
    _db.save(_f)
    _f.close()
    del _db
    # rename current db file to .bak
    if os.path.exists(_bak_fname): os.unlink(_bak_fname)
    os.rename(_fname, _bak_fname)
    # expose the serialized storage file
    os.rename(_tmp_fname, _fname)

class MetaKitDriver:

    """Store information in a MetaKit Database
    
    Concurrent access may be supported by setting MODE_RW=2
    in MetaKit RDF driver.
    """

    def __init__(self, dbName):
        # string storage
        self._dbFile = MetaKit.CreateFileName(dbName)
        self._db = metakit.storage(self._dbFile, MetaKit.MODE_RW)
        # restructure the view if needed
        _vw = self._db.getas(STRING_VIEW)
        self._db.commit()
        # RDF models
        _db = MetaKit.DbAdapter(dbName, SYSTEM_MODEL_NAME)
        # XXX i do not really like the storage object sharing like this
        #   shouldn't we call .begin() method instead?
        _db._db = self._db
        self._systemRdfModel = Model.Model(_db)
        _db = MetaKit.DbAdapter(dbName, USER_MODEL_NAME)
        _db._db = self._db
        self._userRdfModel = Model.Model(_db)

    def mk_strings_view(self):
        """return the MetaKit view containing contents data"""
        return self._db.view("ftss_strings")

    def createFile(self, path, typ, content):
        """When creating a resource, store the content"""
        # Verify that we were given a string object
        if type(content) != str:
            raise TypeError("Content must be a string, not %s" % type(content))
        _vw = self.mk_strings_view()
        _path = enc_utf8(path)
        # duplicates aren't allowed, are they?
        _idx = _vw.find(path=_path, type=typ)
        if _idx >= 0: # row found
            # XXX should we raise an exception better
            #   than silently overwrite old resource?
            _vw[_idx].data = content
        else: # new row
            _vw.append(path=_path, type=typ, data=content)

    def hasFile(self, path, typ):
        """return True if the resource exists"""
        _vw = self.mk_strings_view()
        return (_vw.find(path=enc_utf8(path), type=typ) >= 0)

    def fetchFile(self, path, typ):
        """return contents"""
        _vw = self.mk_strings_view()
        _idx = _vw.find(path=enc_utf8(path), type=typ)
        if _idx >= 0: # row found
            return _vw[_idx].data
        else:
            return None

    def updateFile(self, path, typ, content):
        """Update only the content about this resource"""
        # Verify that we were given a string object
        if type(content) != str:
            raise TypeError("Content must be a string, not %s" % type(content))
        _vw = self.mk_strings_view()
        _idx = _vw.find(path=enc_utf8(path), type=typ)
        if _idx >= 0: # row found
            _vw[_idx].data = content

    def deleteFile(self, path, typ):
        """Delete an object"""
        _vw = self.mk_strings_view()
        _idx = _vw.find(path=enc_utf8(path), type=typ)
        if _idx >= 0: # row found
            del _vw[_idx]

    def getSystemModel(self):
        return self._systemRdfModel

    def getUserModel(self):
        return self._userRdfModel

    def commit(self):
        # due to the storage object sharing (see comments in __init__)
        # we have to commit only once.
        self._db.commit()
        self._close_db()

    def rollback(self):
        # due to the storage object sharing (see comments in __init__)
        # we have to rollback only once.
        # Note: In fact, MetaKit rollback is performed automatically
        #   upon release of the storage object
        self._db.rollback()
        self._close_db()

    def _close_db(self):
        """release all storage objects and RDF models"""
        self._db = None
        self._systemRdfModel._db = None
        self._systemRdfModel = None
        self._userRdfModel._db = None
        self._userRdfModel = None

def Begin(**properties):
    """Begin a new transaction.
    
    Every driver must support this interface.
    The properties keyword arguments are passed from the config file
    (or where ever) to the driver.  The Begin file is responsible
    for doing what ever is needed to validate these arguements
    """
    return MetaKitDriver(properties["DbName"])

NAME="MetaKit"

# vim: set ts=4 et :
