# -*- coding: utf8 -*-
'''
PSF MODULE
**********************
***  PSF MODULE
**********************
$RCSfile: psf.py,v $
$Revision: 945 $
$Author: markus $
$Date: 2008-11-19 18:28:10 +0100 (Mi, 19. Nov 2008) $

Functions associated in reading and parsing 
the system wide configuration file "ProviderConfiguration.xml".
'''

import os, os.path
from xml.sax    import parse
from datetime        import datetime

from biocase.wrapper.errorclasses        import *
from biocase.tools.various_functions     import unique, isTrue
from biocase.tools.xmlutils              import element, subelement, prettyprint, cleanupNamespaces
from biocase.tools.caching               import getModifiedDatetime
from biocase.wrapper.psf_handler         import providerConfigHandler, SupportedSchema
from biocase.wrapper.graph.tablegraph    import tableGraphClass, tableAliasClass
import biocase.configuration

import logging
log=logging.getLogger("pywrapper.psf")

cfg = biocase.configuration.Cfg()

# ----------------------------------------------------------------------------
class ProviderSetup(object):
    PSFnamespace = "http://www.biocase.org/schemas/providersetup/1.5"

    def __init__(self, dsaObj):
        # datasource object knows about dsa name, configdir
        self.dsaObj = dsaObj
        # the absolute filepath to relevant directories and files
        # they will be build by reading a specific database directory with method 'setFileDirPaths'
        self.absPSFilename  = None
        #
        # defaults overridden in provider_setup_file.xml
        self.adminpassword  = None
        self._loglevel      = None
        self.recLimit       = cfg.server.recLimit
        # host part (properties)
        self._user          = None
        self._password      = None
        self._database      = None
        self._dbIP          = None
        self.dbms           = None
        self.encoding       = None
        # schema part
        self.schemas      = {} # key=namespace, value = SupportedSchema Object
        # table part represented as an tablegraph object
        self.tablegraph     = tableGraphClass()

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getUser(self):
        return self._user
    def setUser(self, x):
        if x is None: x=""
        self._user=x
    def getPassword(self):
        return self._password
    def setPassword(self, x):
        if x is None: x=""
        self._password=x
    def getDatabase(self):
        return self._database
    def setDatabase(self, x):
        if x is None: x=""
        self._database=x
    def getDbIp(self):
        return self._dbIP
    def setDbIp(self, x):
        if x is None: x=""
        self._dbIP=x
    def getLogLevel(self):
        return self._loglevel
    def setLogLevel(self, x):
        try:
            self._loglevel=int(x)
        except:
            pass

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def __repr__(self):
        return "PSF: PSF=%s, recLimit=%s, loglevel=%s, user=%s, database=%s, dbIP=%s, dbms=%s, encoding=%s, schemas=%s, tablegraph=%s" % (self.absPSFilename, self.recLimit, self.loglevel, self.user, self.database, self.dbIP, self.dbms, self.encoding, unicode(self.schemas), self.tablegraph)

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def repr(self):
        from biocase.tools.showobject import view
        return view(self)    
    
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def readPSFile(self, filename=None):
        # use SAX to parse the providersetup.xml file and fill this object
        if filename == None:
            filename = self.absPSFilename
        else:
            self.absPSFilename = filename
        log.info("Reading PSF from %s"%filename)
        handler = providerConfigHandler(self)
        parse(filename, handler)
        try:
            handler = providerConfigHandler(self)
            parse(filename, handler)
            log.debug(str(self))
        except:
            raise ProviderSetupParseError(filename)

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def writePSFile(self, filename=None, namespace=None):
        '''Writes the PSF object to an xml file.
        If no namespace is given it uses by default the latest PSF version.'''
        if filename is None:
            filename = self.absPSFilename
        PSFile = file(filename,'w')
        log.debug("writing PSFile to %s" %(filename) )
        psfET = self.createPSFET()
        cleanupNamespaces(psfET)
        prettyprint(psfET, PSFile)
        PSFile.close()
        
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def cleanup(self):
        '''Cleans up inconsistent PSF data without raising errors - like table without primary keys.'''
        status = True
        # verify used schemas
        used_namespaces={}
        for ns, schema in self.schemas.items():
            # check if file exists
            if not os.path.isfile(schema.conceptMappingFile):
                del self.schemas[ns]
                status = False
            # check for duplicate namespaces
            if used_namespaces.has_key(ns):
                del self.schemas[ns]
                status = False
            else:
                used_namespaces[ns] = 1
        # very tablealiases and graph structure
        for table in self.tablegraph.getTableAliasObjects():
            if len(table.pk) < 1 or table.table is None or table.alias is None:
                self.tablegraph.delAlias(table.alias)
                status = False
            for fk in table.fks:
                if fk.target is None or len(fk.attrList) < 1 or self.tablegraph.getTableAliasObj(fk.target) is None:
                    table.fks.remove(fk)
                    status = False
        return status
        
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getCMFfilename(self, namespace):
        '''Returns the full CMF filename used for a given namespace.
        If no CMF file is found None is returned'''
        log.info("Try to get CMF for namespace %s"%namespace)
        if self.schemas.has_key(namespace):
            return self.schemas[namespace].conceptMappingFile
        else:
            if self.schemas.has_key(namespace.lower()):
                return self.schemas[namespace.lower()].conceptMappingFile
            else:
                log.warn("No CMF found! Available CMFs: %s" %unicode(self.schemas.keys()))
                return None

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getPSFUpdatedWhen(self):
        '''Returns the datetime of the last update of the PSF file or None is unknown.'''
        if os.path.isfile(self.absPSFilename):
            return getModifiedDatetime(self.absPSFilename)
        else:
            return None

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getTableAliasObj(self, alias):
        '''Returns the table alias object for a given alias or None.'''
        try:
            return self.tablegraph.getTableAliasObj(alias)
        except:
            return None

    # ------------------------------------    
    def getTableAliasList(self):
        '''Returns a list of all table alias objects or [].'''
        try:
            return self.tablegraph.getTableAliasList()
        except:
            return []
        
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getCompletePKDBAttrObjList(self):
        '''Returns a list of all DBAttrObjects used for the primary keys of all table aliases.'''
        return self.getPKDBAttrObjListOfDBAttrObjList( aliasList=self.getTableAliasList() )
        
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getPKDBAttrObjListOfAlias(self, alias):
        '''Returns a list of DBAttrObjects for the primary keys of a given table alias (as an object or string).
        Returns [] if alias is not existing.'''
        pks = self.tablegraph.getTablePKsDict()
        if isinstance(alias, tableAliasClass):
            alias = alias.alias
        try:
            return pks[alias]
        except:
            return []

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getPKDBAttrObjListOfDBAttrObjList(self, dbaoList=[], aliasList=[]):
        '''Returns a distinct list of DBAttrObjects for the primary keys of a given list of db-attribute objects.
        Returns [] if no tablealias is existing.'''
        pks = []
        # get the list of all table aliases
        for DBAttrObj in dbaoList:
            aliasList.append(DBAttrObj.table)
        # make them unique
        aliasList = unique(aliasList)
        # get the list of all PK-DBAttrObjects
        for alias in aliasList:
            pks += self.getPKDBAttrObjListOfAlias(alias)
        return pks
        
    # ------------------------------------    
    # graph related methods
    # ------------------------------------    

    # ------------------------------------    
    def buildGraph(self, aliasObjList):
        '''Build a new graph from a list of table alias objects overwriting any existing graph.'''
        self.tablegraph.buildGraph(aliasObjList)

    # ------------------------------------    
    def clearGraph(self):
        '''Remove nonsense data from tablegraph.'''
        self.tablegraph.clearGraph()

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def circlesExist(self):
        '''Returns 1 if the tablegraph contains circular structures.'''
        return self.tablegraph.circlesExist()
        
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getTableTreeObj(self, rootTableAliasName, staticTableAliasList=[]):
        '''function to create and return a tabletree object with 
        - a given root table-alias name and 
        - an optional list of static table aliases.
        Throws an error if a circular graph was given or if unknown aliases were passed.
        '''
        return self.tablegraph.getTableTree(rootTableAliasName, staticTableAliasList)


    # ------------------------------------    
    # schema related methods
    # ------------------------------------    

    # ------------------------------------    
    def delAllSchemas(self):
        '''Removes all supprted schemas of this psf.'''
        self.schemas = {}

    # ------------------------------------    
    def addSchema(self, schemaNS, file, request=1, response=1):
        '''Adds a supported schema to the psf.'''
        ss = SupportedSchema()
        ss.request = request
        ss.response= response
        ss.name = schemaNS
        ss.conceptMappingFile = file
        if self.schemas.has_key(schemaNS) and self.schemas[schemaNS] is not None:
            raise MultipleCMFsWithSameNamespaceError()
        self.schemas[schemaNS] = ss


    # ------------------------------------    
    # write methods
    # ------------------------------------    

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def createPSFET(self):
        '''    Returns a ElementTree object that represents this psf object as a ProviderSetupFile.
        '''
        return self.createPSFET_1_5()
        
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def createPSFET_1_5(self):
        '''Return a PSF ElementTree object with namespace version 1.5'''
        ns = self.__class__.PSFnamespace
        psf_node = element(ns, "Provider", adminpassword=self.adminpassword, loglevel=self.loglevel)
        host = subelement(psf_node, ns, "Host")
        subelement(host, ns, "User", self.user)
        subelement(host, ns, "Password", self.password)
        subelement(host, ns, "Datasource", self.database)
        subelement(host, ns, "DB_IP", self.dbIP)
        subelement(host, ns, "DBMS", self.dbms)
        db_node = subelement(psf_node, ns, "DB")
        subelement(db_node, ns, "TextEncoding", self.encoding)
        # table aliases
        tables = self.getTableAliasList()
        for t in tables:
            ta_node = subelement(db_node, ns, "TableAlias", tablename=t.table, alias=t.alias)
            # PKS
            pk_node = subelement(ta_node, ns, "PrimaryKey")
            for pk in t.pk:
                subelement(pk_node, ns, "Attribute", pk.attr, dbType=pk.type)
            # FKS
            for fkObj in t.fks:
                fk_node = subelement(ta_node, ns, "ForeignKey", target=fkObj.target)
                for attrObj in fkObj.attrList:
                    subelement(fk_node, ns, "Attribute", attrObj.attr, dbType=attrObj.type)
        # SCHEMAS
        schemas_node = subelement(psf_node, ns, "SupportedSchemas")
        for ss in self.schemas.values():
            ss_node = subelement(schemas_node, ns, "Schema", os.path.split(ss.conceptMappingFile)[1], namespace=ss.name, request=ss.request, response=ss.response)
        # WRAPPER BEHAVIOR
        behavior_node = subelement(psf_node, ns, "WrapperBehavior")
        if self.recLimit is not None:
            subelement(behavior_node, ns, "RecordResultLimit", str(self.recLimit))
        return psf_node

    def getDsaDir(self):
        if self.dsaObj is not None:
            return self.dsaObj.getDsaDir()
        return None
    def getDsaName(self):
        if self.dsaObj is not None:
            return self.dsaObj.name
        else:
            return absPSFilename
    password        = property(getPassword, setPassword)
    sourcename      = property(getDsaName)
    user            = property(getUser, setUser)
    database        = property(getDatabase, setDatabase)
    dbIP            = property(getDbIp, setDbIp)
    loglevel        = property(getLogLevel, setLogLevel)
    configDir       = property(getDsaDir)
