# Copyright (c) 2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany).
# Copyright (c) 2001-2005 LOGILAB S.A. (Paris, FRANCE).
#
# http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""Narval RC file : allow control of narval startup, i.e. which cookbooks,
modules, elements and protocol handlers will be loaded


:version: $Revision:$ 
:author: Logilab  

:copyright:
  2001-2005 LOGILAB S.A. (Paris, FRANCE)
  
  2004-2005 DoCoMo Euro-Labs GmbH (Munich, Germany)
  
:contact:
  http://www.logilab.fr/ -- mailto:contact@logilab.fr
  
  http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com
"""

__revision__ = "$Id: narvalrc.py 20 2004-04-15 14:43:51Z syt $"
__docformat__ = "restructuredtext en"

from os.path import normpath, join
from cStringIO import StringIO
from xml.sax import ContentHandler, make_parser

from narval.config import get_home
from narval.utils import Singleton



class DictSaxHandlerMixin(ContentHandler):
    """easily construct xml config file parsing by inheriting from this class
    
    The subclass shoud define 3 dictionaries, 2 tuples and a string variable
    which explain the document structure.
    This handler is initialized with an object which can be handled as a
    dictionary, and will contain configuration information at the end of
    parsing.

    required attributes:

    :type _MASTER: dict
    :cvar _MASTER:
      dictionary designing the first level key for instance, with _MASTER ==
      {'part1':'name', 'part2':None} the handler, when it receives a
      startElement with name == 'part1', will append to the handler stack
      pref_o.prefs[attrs.get(_MASTER[name])], where pref_o is the preference
      object given to __init__ and attrs the attributes dictionary given to
      startElement. On the other hand, when it receives a startElement with
      name=='part2', it will append pref_o.prefs[name] to the handler stack.

    :type _KEY: tuple or list
    :cvar _KEY: tuple designing last level key

    :type _LIST: tuple or list
    :cvar _LIST:
      tuple designing multiple last level key, same as above, but value will be
      append to a list

    :type _CB: dict
    :cvar _CB: 
      dictionary where key are element of _KEY or _LIST and value function
      to call when retreiving the element value.

    All first level key should be predefined in the preferences dictionary
    """
    
    def __init__(self, pref_o=None):
        ContentHandler.__init__(self)
        self._stack = None
        self._ALL = None
        self.last_key = None
        if pref_o is not None:
            self.init_state(pref_o)
                
    def init_state(self, pref_o):
        """init handler state using the given preference object

        :type pref_o: dict
        :param pref_o: the object which will handle the preferences information
        """
        self._stack = [pref_o]
        self._ALL = {}
        self.last_key = None
        for l in (self._MASTER.keys(), self._S_LIST, self._LIST):
            for e in l:
                self._ALL[e] = 1

        
    def startElement(self, name, attrs):
        """SAX callback: start a new xml node

        :type name: unicode
        :param name: the tag name
        
        :type attrs: dict
        :param attrs: the node's attribute values, indexed by attribute's name
        """
        self.last_key = None
        ##
        if self._MASTER.has_key(name):
            key = self._MASTER[name]
            if key:
                self._stack.append(self._stack[-1].setdefault(attrs.get(key),{}))
            else:
                self._stack.append(self._stack[-1].setdefault(name, {}))
        elif name in self._S_LIST:
            l = self._stack[-1].setdefault(name, [])
            l.append({})
            self._stack.append(l[-1])
        elif name in self._LIST:
            self.last_key = name
            self._stack.append(self._stack[-1].setdefault(name, []))
        ##    
        if name in self._KEY:
            self.last_key = name
            
    def endElement(self, name):
        """SAX callback: close a xml node

        :type name: unicode
        :param name: the tag name
        """
        if self._ALL.has_key(name):
            self._stack.pop()

    def characters(self, content):
        """SAX callback: get some characters

        :type content: unicode
        :param content: the non empty string to hold
        """
        if self.last_key:
            content = ' '.join(content.split())
            if content:
                key = self.last_key
                if self._CB.has_key(key):
                    content = self._CB[key](content)
                if key in self._LIST:
                    self._stack[-1].append(content)
                else:
                    self._stack[-1][key] = content

class PrefReader:
    """the preference reader : associate a SAX handler to a preference object
    to be able to load configuration from string / files
    """
    def __init__(self, handler_class, pref_o):
        self._p = make_parser()
        self._h = handler_class(pref_o)
        self._p.setContentHandler(self._h)

    def from_file(self, path):
        """read preferences from file

        :type path: str
        :param path: path to the preference file
        """
        self._p.parse(open(path))

    def from_string(self, string):
        """read preferences from string

        :type string: str
        :param string: XML document containing configuration information
        """
        self._p.parse(StringIO(string))


# configuration object #########################################################

class NarvalRCSaxHandler(DictSaxHandlerMixin):
    """special SAX handler for narval rc file"""
    
    _MASTER = {'enable':None, 'disable':None}
    _S_LIST = ()
    _LIST = ('interfaces', 'elements', 'actions' ,
             'cookbook', 'protocol-handler')
    _KEY = ('encoding', 'package', 'class')
    _CB = {}


class NarvalRC(Singleton):
    """singleton class for narval rc file information handling, implementing
    the dictionary interface to access / set configuration parameters
    """

    def __init__(self, filename=None):
        super(NarvalRC, self).__init__()
        self._prefs = {'encoding': 'UTF-8',
                       'disable' : {'actions': [],
                                    'elements' : [],
                                    'protocol-handler' : [],
                                    'interfaces' : [],
                                    'cookbook' : []},
                       'enable' : {'actions': [],
                                   'interfaces' : [],
                                   'protocol-handler' : [],
                                   'elements' : [],
                                   'cookbook' : []},
                       }
        self.black_list = self._prefs['disable']
        self.white_list = self._prefs['enable']
        if filename is None:
            filename = normpath(join(get_home(), 'data', 'narvalrc.xml'))
        reader = PrefReader(NarvalRCSaxHandler, self)
        self.mode = 'allow_all'
        try:
            reader.from_file(filename)
            for v in self.white_list.values():
                if v:
                    self.mode = 'deny_all'
                    break
        except IOError:
            log(LOG_NOTICE,
                'no configuration file found, everything will be loaded')
        
    def __getitem__(self, key):
        return self._prefs[key]

    def __setitem__(self, key, val):
        self._prefs[key] = val

    def setdefault(self, key, val):
        """set the default value for the given key if necessary and return the
        actual value

        :type key: unicode or str
        :param key: dictionary key

        :type val: unicode
        :param val: key's default value

        :rtype: unicode or str
        :return: the actual key's value
        """
        return self._prefs.setdefault(key, val)

    def _load(self, category, obj):
        """return true if the object from the given category should be loaded

        :type category: str
        :param category:
          name of the category ('interfaces', 'elements', 'actions', 'cookbook'
          or 'protocol-handler')

        :type obj: str
        :param obj: name of the object

        :rtype: bool
        :return: a flag indicating whether the cookbook should be loaded
        """
        if self.mode == "allow_all":
            return not obj in self.black_list[category]
        return obj in self.white_list[category]

    def load_cookbook(self, cookbook):
        """return true if the cookbook should be loaded

        :type cookbook: str
        :param cookbook: name of the cookbook

        :rtype: bool
        :return: a flag indicating whether the cookbook should be loaded
        """
        return self._load('cookbook', cookbook)

    def load_actions_module(self, module):
        """return true if the action module should be loaded

        :type module: str
        :param module: name of the module

        :rtype: bool
        :return: a flag indicating whether the module should be loaded
        """
        return self._load('actions', module)
        
    def load_elements_module(self, module):
        """return true if the elements module should be loaded

        :type module: str
        :param module: name of the module

        :rtype: bool
        :return: a flag indicating whether the module should be loaded
        """
        return self._load('elements', module)
        
    def load_interfaces_module(self, module):
        """return true if the interfaces module should be loaded

        :type module: str
        :param module: name of the module

        :rtype: bool
        :return: a flag indicating whether the module should be loaded
        """
        return self._load('interfaces', module)

    def load_protocol_handler_module(self, module):
        """return true if the protocol handler module should be loaded

        :type module: str
        :param module: name of the module

        :rtype: bool
        :return: a flag indicating whether the module should be loaded
        """
        return self._load('protocol-handler', module)
        
