########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Server/Lib/ConfigFile.py,v 1.27 2005/04/07 20:35:49 jkloth Exp $
"""
Processing of the global 4Suite repository config file,
for aggregation in Ft.Server.Server.GlobalConfig

Copyright 2005 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

import os, re

from Ft import GetConfigVar
from Ft.Lib import Time, Uri
from Ft.Rdf.Serializers.Dom import Serializer
from Ft.Rdf import RDF_MS_BASE, RDF_SCHEMA_BASE
from Ft.Server.Server import FtServerServerException, Error
from Ft.Server.Server.Lib import LogUtil
from Ft.Xml import Domlette

CONFIG_MODEL_BASE = 'http://xmlns.4suite.org/4ss/properties#'

# Functions to read the config file
def GetFileName(fileName):
    """Locate the config file"""
    if not fileName:
        fileName = os.environ.get("FTSERVER_CONFIG_FILE",
                                  os.path.join(GetConfigVar('SYSCONFDIR'),
                                               '4ss.conf'))
    if not os.path.exists(fileName):
        raise FtServerServerException(Error.CONFIG_FILE_MISSING, file=fileName)
    return fileName


def Read(fileName=None):
    """This function will find and read in the configuration file.  It will return a dictionary
    where each entry is keyed by ID and the value is a dictionary that represents the
    values of that Core"""

    config = {}

    fileName = GetFileName(fileName)
    model = ReadConfigFile(fileName)

    for core in model.complete(None, RDF_MS_BASE + 'type',
                               CONFIG_MODEL_BASE + 'Core'):
        coreId = core.subject
        core_id_list = coreId.split('#')
        if len(core_id_list) == 2:
            coreId = core_id_list[1]
        config[coreId] = ReadCore(core.subject, model)
        config[coreId]['CoreId'] = coreId
        config[coreId]['ConfigFile'] = fileName

    return config


def ReadConfigFile(fileName):
    """Load the RDF config file into a model"""
    from Ft.Rdf import Util
    model, db = Util.DeserializeFromUri(Uri.OsPathToUri(fileName))
    return model


def ReadCore(coreId, model):
    """Read in a single Core resource"""

    core = {}

    # Load the path properties
    properties = ['PidFile', 'LogFile']
    for property in properties:
        core[property] = ReadProperty(model, coreId, property)


    # Ensure all given paths are absolute
    for (property, path) in core.items():
        if not os.path.isabs(path):
            raise FtServerServerException(Error.CONFIG_INVALID_VALUE,
                                          property=property,
                                          reason='path must be absolute')

    # Get the maximum logging level for the Controller
    level = ReadProperty(model, coreId, 'LogLevel', required=0)
    if not level:
        # Not specified, use default maximum level
        loglevel = LogUtil.LOG_NOTICE
    else:
        try:
            loglevel = LogUtil.FromString(level)
        except ValueError, error:
            raise FtServerServerException(Error.CONFIG_INVALID_VALUE,
                                          property='LogLevel',
                                          reason=str(error))
    core['LogLevel'] = loglevel

    # Get the listening address(es) for all servers (assume valid host or IP)
    core['ListenAddress'] = ReadProperties(model, coreId, 'ListenAddress', 0)

    # Get the interval for the temporary file purge thread, in seconds
    reaper_delay = ReadProperty(model, coreId, 'TemporaryReapInterval', required=0)
    if not reaper_delay:
        # Not specified, disable
        reaper_delay = 0
    else:
        try:
            reaper_delay = int(reaper_delay)
        except ValueError, error:
            raise FtServerServerException(Error.CONFIG_INVALID_VALUE,
                                          property='TemporaryReapInterval',
                                          reason=str(error))
    core['TemporaryReapInterval'] = reaper_delay

    # Get the interval for dynamic reload checking, in seconds
    reload_delay = ReadProperty(model, coreId, 'DynamicReloadInterval', required=0)
    if not reload_delay:
        # Not specified, disable
        reload_delay = 0
    else:
        try:
            reload_delay = int(reload_delay)
        except ValueError, error:
            raise FtServerServerException(Error.CONFIG_INVALID_VALUE,
                                          property='DynamicReloadInterval',
                                          reason=str(error))
    core['DynamicReloadInterval'] = reload_delay


    # Get the interval for executing DB Maintenance
    db_delay = ReadProperty(model, coreId, 'DbMaintenanceTime', required=0)
    if not db_delay:
        # Not specified, disable
        db_delay = 0
    else:
        try:
            db_delay = Time.FromISO8601(db_delay)
            db_delay = db_delay.asPythonTime(0)
        except ValueError, error:
            raise FtServerServerException(Error.CONFIG_INVALID_VALUE,
                                          property='DbMaintenanceTime',
                                          reason=str(error))
    core['DBMaintenanceTime'] = db_delay


    # Get the interval for the xslt strobe, in seconds
    xslt_strobe_int = ReadProperty(model, coreId, 'XsltStrobeInterval', required=0)
    if not xslt_strobe_int:
        # Not specified, disable
        xslt_strobe_int = 0
    else:
        try:
            xslt_strobe_int = int(xslt_strobe_int)
        except ValueError, error:
            raise FtServerServerException(Error.CONFIG_INVALID_VALUE,
                                          property='XsltStrobeInterval',
                                          reason=str(error))
    core['XsltStrobeInterval'] = xslt_strobe_int

    # Optional arguments for pool size regulation
    for name in ('StartServers', 'MinSpareServers', 'MaxSpareServers',
                 'MaxServers'):
        value = ReadProperty(model, coreId, name, required=0)
        if value:
            try:
                value = int(value)
            except ValueError, error:
                raise FtServerServerException(Error.CONFIG_INVALID_VALUE,
                                              property=name,
                                              reason=str(error))
            core[name] = value

    # Load the driver
    driverId = ReadProperty(model, coreId, 'Driver')
    core['Driver'] = ReadDriver(driverId, model)

    property = 'XsltExtensionModule'
    core[property] = ReadProperties(model, coreId, property, required=0)
    return core


def ReadProperties(model, id, property, required=1):
    statements = model.complete(id, CONFIG_MODEL_BASE + property, None)
    if not statements:
        if required:
            raise FtServerServerException(Error.CONFIG_MISSING_PROPERTY,
                                          property=property)
        else:
            result = []
    else:
        result = [ statement.object for statement in statements ]
    return result


def ReadProperty(model, id, property, required=1):
    # ReadProperties will raise an exception if needed
    props = ReadProperties(model, id, property, required)
    if not props:
        result = None
    elif len(props) > 1:
        raise FtServerServerException(Error.CONFIG_MULTIPLE_PROPERTIES,
                                      property=property)
    else:
        result = props[0]
    return result


def ReadDriver(driverId, model):
    """Read a driver Entry"""
    dType = model.complete(driverId,RDF_MS_BASE+'type',None)

    if len(dType) != 1:
        raise FtServerServerException(Error.CONFIG_MISSING_PROPERTY,property='Driver Type')
    if dType[0].object == CONFIG_MODEL_BASE + 'FlatFile':
        res = ReadFlatFileDriver(driverId, model)
        res['TYPE'] = 'FlatFile'
    elif dType[0].object == CONFIG_MODEL_BASE + 'Postgres':
        res = ReadPostgresDriver(driverId, model)
        res['TYPE'] = 'Postgres'
    elif dType[0].object == CONFIG_MODEL_BASE + 'Psyco':
        res = ReadPsycoDriver(driverId, model)
        res['TYPE'] = 'Psyco'
    elif dType[0].object == CONFIG_MODEL_BASE + 'Oracle':
        res = ReadOracleDriver(driverId, model)
        res['TYPE'] = 'Oracle'
    elif dType[0].object == CONFIG_MODEL_BASE + 'Odbc':
        res = ReadOdbcDriver(driverId, model)
        res['TYPE'] = 'Odbc'
    elif dType[0].object == CONFIG_MODEL_BASE + 'MetaKit':
        res = ReadMetaKitDriver(driverId, model)
        res['TYPE'] = 'MetaKit'
    elif dType[0].object == CONFIG_MODEL_BASE + 'MySQL':
        res = ReadMySQLDriver(driverId, model)
        res['TYPE'] = 'MySQL'
    elif dType[0].object == CONFIG_MODEL_BASE + 'BerkeleyDB':
        res = ReadBerkeleyDBDriver(driverId, model)
        res['TYPE'] = 'BerkeleyDB'        
    else:
        raise FtServerServerException(Error.UNKNOWN_DRIVER_ERROR,
                                      driver=dType[0].object)
    return res


SQL_ID_REGEX = '[A-Za-z_\x80-\xFF][A-Za-z_\x80-\xFF0-9$]*'

def _validateSqlId(s, prop):
    """
    Raises an exception if the given string is not a valid SQL ID.

    prop = property name (e.g. 'DbName') for which s is the value.
    """
    if re.match(SQL_ID_REGEX, s) is None:
        raise FtServerServerException(Error.CONFIG_INVALID_VALUE,
                                      property=prop, reason=s)
    return


def ReadFlatFileDriver(driverId, model):
    """Read a FlatFile driver configuration"""
    flatfile = {}
    flatfile['Root'] = ReadProperty(model, driverId, 'Root')
    _validateSqlId(flatfile['Root'], 'Root')
    return flatfile


def ReadPostgresDriver(driverId, model):
    """Read a Postgres driver configuration"""
    p = {}
    p['DbName'] = ReadProperty(model, driverId, 'DbName')
    _validateSqlId(p['DbName'], 'DbName')
    p['User'] = ReadProperty(model, driverId, 'User',0)
    p['Passwd'] = ReadProperty(model, driverId, 'Passwd',0)
    p['Host'] = ReadProperty(model, driverId, 'Host',0)
    p['Port'] = ReadProperty(model, driverId, 'Port',0)
    if p['Port'] is None: p['Port'] = -1
    return p

def ReadMySQLDriver(driverId, model):
    """Read a Postgres driver configuration"""
    p = {}
    p['DbName'] = ReadProperty(model, driverId, 'DbName')
    _validateSqlId(p['DbName'], 'DbName')
    p['User'] = ReadProperty(model, driverId, 'User',0)
    p['Passwd'] = ReadProperty(model, driverId, 'Passwd',0)
    p['Host'] = ReadProperty(model, driverId, 'Host',0)
    p['Port'] = ReadProperty(model, driverId, 'Port',0)
    if p['Port'] is None: p['Port'] = 3306
    return p

def ReadBerkeleyDBDriver(driverId,model):
    """Read a BerkeleyDB driver configuration"""
    p = {}
    p['OneMBDocuments']      = ReadProperty(model, driverId, 'OneMBDocuments',0)
    p['DataStoreContainers'] = ReadProperty(model, driverId, 'DataStoreContainers',0)
    p['DbXml'] = ReadProperty(model, driverId, 'DbXml',0)    
    p['Root']                = ReadProperty(model, driverId, 'Root',0)
    return p

def ReadPsycoDriver(driverId, model):
    """Read a Psyco driver configuration"""
    p = {}
    p['DbName'] = ReadProperty(model, driverId, 'DbName')
    _validateSqlId(p['DbName'], 'DbName')
    p['User'] = ReadProperty(model, driverId, 'User',0)
    p['Passwd'] = ReadProperty(model, driverId, 'Passwd',0)
    p['Host'] = ReadProperty(model, driverId, 'Host',0)
    p['Port'] = ReadProperty(model, driverId, 'Port',0)
    if p['Port'] is None: p['Port'] = -1
    return p


def ReadOracleDriver(driverId, model):
    """Read an Oracle driver configuration"""
    p = {}
    p['DbName'] = ReadProperty(model, driverId, 'DbName')
    _validateSqlId(p['DbName'], 'DbName')
    p['User'] = ReadProperty(model, driverId, 'User',0)
    p['Passwd'] = ReadProperty(model, driverId, 'Passwd',0)
    p['Host'] = ReadProperty(model, driverId, 'Host',0)
    p['Port'] = ReadProperty(model, driverId, 'Port',0)
    if p['Port'] is None: p['Port'] = -1
    return p


def ReadOdbcDriver(driverId, model):
    """Read an ODBC driver configuration"""
    conf = {}
    conf['DataSource'] = ReadProperty(model, driverId, 'DataSource')
    conf['User'] = ReadProperty(model, driverId, 'User',0)
    conf['Password'] = ReadProperty(model, driverId, 'Password',0)
    conf['Host'] = ReadProperty(model, driverId, 'Host',0)
    return conf


def ReadMetaKitDriver(driverId, model):
    """Read a MetaKit driver configuration"""
    conf = {}
    conf['DbName'] = ReadProperty(model, driverId, 'DbName')
    _validateSqlId(conf['DbName'], 'DbName')
    return conf
