########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Server/Http/HeaderDict.py,v 1.6 2005/03/06 03:53:55 mbrown Exp $
"""
MIME message header storage for HTTP

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

from __future__ import generators

class HeaderDict(dict):
    """
    A dictionary-like object for storing MIME-like message headers.
    Headers are stored in the object via regular dictionary interfaces.
    They are retrieved via str(), repr() or self.terse(), which returns
    a standard dict with duplicate headers merged into a comma-separated
    list of values (even when it is not OK to do so).

    The reason this class is needed is because the dictionary interface of
    the Message class of the rfc822 and mimetools modules allows each new
    header to overwrite a previously-stored header with the same key. Our
    HeaderDict class stores its header values as lists, so no data is lost.
    Its keys are accessed case-insensitively and are always written out
    in a normalized format where usually only the first letter of each
    hyphen-separated word is capitalized. (Content-MD5, ETag, TE, and
    WWW-Authenticate are exceptions to this rule). The str(obj)
    representation prints the headers in a convenient format suitable for
    printing in a MIME-like message.
    """

    # exceptions to the automatic camel-capping
    forceCaps = { 'content-md5': 'Content-MD5',
                  'etag': 'ETag',
                  'te': 'TE',
                  'www-authenticate': 'WWW-Authenticate'
                }

    # camel-cap a hyphenated string
    def _camelCap(self, s, checkForceCaps=False):
        if len(s) == 0:
            return ''
        if checkForceCaps:
            if s.lower() in forceCaps.keys():
                return forceCaps[s.lower()]
        return '-'.join(map(lambda s: s[:1].upper() + s.lower()[1:], s.split('-')))

    def __init__(self, obj=None):
        if obj is None:
            return super(HeaderDict, self).__init__()
        elif isinstance(obj, (tuple, list, str, unicode)):
            for k, v in obj:
                self.__setitem__(k, v)
        else:
            # will raise same TypeError as dict, if not iterable
            for k in obj:
                self.__setitem__(k, obj[k])
        return

    # case-insensitive lookup
    def __getitem__(self, key):
        return super(HeaderDict, self).__getitem__(key.lower())

    # case-insensitive 'key in dict' lookup
    def __contains__(self, key):
        return super(HeaderDict, self).__contains__(key.lower())

    # case-insensitive 'for key in dict' iteration
    def __iter__(self):
        return iter(self.keys())

    # case-insensitive iterators
    iterkeys = __iter__
    def iteritems(self):
        for key in self.iterkeys():
            yield key, self.__getitem__(key)

    # case-insensitive storage; appends to a list
    def __setitem__(self, key, value):
        if self.has_key(key):
            l = super(HeaderDict, self).__getitem__(key.lower())
            l.append(value)
            return super(HeaderDict, self).__setitem__(key.lower(), l)
        else:
            return super(HeaderDict, self).__setitem__(key.lower(), [value])

    # case-insensitive deletion: removes entire list
    def __delitem__(self, key):
        return super(HeaderDict, self).__delitem__(key.lower())

    # retrieve a value or default if no such key
    def get(self, key, default=None):
        if self.has_key(key):
            return self.__getitem__(key)
        else:
            return default

    # case-insensitive key lookup
    def has_key(self, key):
        return super(HeaderDict, self).has_key(key.lower())

    # normalize key names
    def keys(self):
        return [self._camelCap(key, False) for key in super(HeaderDict, self).keys()]

    # return a terse dictionary (duplicate headers in one comma-separated string)
    # this is for compatibility with functions want to read the header values as
    # a single string rather than as a python list of strings
    def terse(self):
        newdict = {}
        for key in self.keys():
            newdict[key] = ', '.join(self.__getitem__(key))
        return newdict

    # append another dict or HeaderDict
    def update(self, dict_):
        if isinstance(dict_, self.__class__):
            for key in dict_:
                for val in dict_[key]:
                    self.__setitem__(key, val)
        elif isinstance(dict_, dict):
            for key in dict_:
                self.__setitem__(key, dict_[key])
        else:
            raise TypeError("HeaderDict.update() called with a non-dictionary argument")
        return

    # string representations
    def __repr__(self):
        # dictionary syntax
        return '{%s}' % ', '.join([("%s: %s" % (repr(key), repr(self.__getitem__(key.lower())))) for key in self.keys()])

    def __str__(self):
        # a series of 'Hyphenated-Header: value' CRLF-terminated lines, or empty string
        rv = ''
        if len(self.keys()):
            for key in self.keys():
                val = self.__getitem__(key.lower())
                rv += ''.join(['%s: %s\r\n' % (key, v) for v in val])
        return rv
