# Copyright (C) 2003 by Intevation GmbH
# Authors:
# Bernhard Herzog <bh@intevation.de>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with the software for details.

"""XML support code for the test cases"""

__version__ = "$Revision: 1.5 $"
# $Source: /thubanrepository/thuban/test/xmlsupport.py,v $
# $Id: xmlsupport.py,v 1.5 2003/08/28 15:20:57 bh Exp $

import sys
import os
from StringIO import StringIO
import xml.sax
import xml.sax.handler
from xml.sax import make_parser, ErrorHandler, SAXNotRecognizedException

import support

try:
    import pyRXP
except ImportError:
    pyRXP = None

class ValidationTest:

    """Mix-in class for tests that want to check for validity of XML data"""

    # If true, at least one test case tried to validate XML data but
    # couldn't because PyRXP is not available
    validation_attempt_ignored = False

    def validate_data(self, data):
        """Validate the XML data"""
        if pyRXP is not None:
            parser = pyRXP.Parser()
            try:
                parser.parse(data, eoCB = self.rxp_eo_cb)
            except pyRXP.error, val:
                raise AssertionError, str(val), sys.exc_info()[2]
        else:
            ValidationTest.validation_attempt_ignored = True

    def rxp_eo_cb(self, filename):
        """Resolve an eternal entity

        In the Thuban test cases the only external entities that have to
        be resolved are the DTDs which now live in the resource
        directory. So, interpret any filename relative to that
        directory.
        """
        return os.path.join(support.resource_dir(), "XML", filename)


#
# Classes and functions to convert XML to a normalized list
# representation for comparisons
#

class SaxEventLister(xml.sax.handler.ContentHandler):

    """Create a normalized list representation containing the SAX events

    The normalization includes the following

     - The attribute dictionary of a staertElement event is converted to
       a sorted list of (key, value) pairs

     - ID and IDREF attribute values are normalized in such a way that
       two documents that only use different values for IDs can still
       lead to the same normalized representation.

       The implementation of this feature assumes that all IDs are
       defined before they are used. The normalized ID values are of the
       form 'D<NUM>' where <NUM> is a counter starting with 0, so the
       first ID value will become 'D0', the second 'D1', etc.

       Which attributes are IDs or IDREFS is defined with the
       correspoding constructor arguments.

     - Filenames are normalized with os.path.normpath. Which attributes
       are filenames is defiend with the corresponding constructor
       argument.
    """

    def __init__(self, ids = (), idrefs = (), filenames = ()):
        """Initialize the SaxEventLister

        The ids and idrefs parameters should be lists of (element, attr)
        pairs where element is the name of an attribute as passed to the
        startElementNS method and attr is the name of an attribute as
        used in the mapping passed to startElementNS, so both name and
        attr usually must include the namespace.

        The filenames parameter should be a sequence of the same form as
        ids and idrefs identifying the attributes which are filenames
        that should be normalized.
        """
        self.eventlist = []
        self.ids = ids
        self.idrefs = idrefs
        self.idremap = {}
        self.filenames = filenames

    def startElementNS(self, name, qname, attrs):
        items = attrs.items()
        items.sort()
        for i, (attr, value) in zip(range(len(items)), items):
            #print '++++'
            #print self.idremap
            #print name, attr, value
            if (name, attr) in self.ids:
                newid = len(self.idremap)
                self.idremap[value] = "D" + str(newid)
                value = self.idremap[value]
            elif (name, attr) in self.idrefs:
                value = self.idremap[value]
            elif (name, attr) in self.filenames:
                value = os.path.normpath(value)
            items[i] = (attr, value)
            #print name, attr, value
        self.eventlist.append(("start", name, items))

    def endElementNS(self, name, qname):
        self.eventlist.append(("end", name))


def sax_eventlist(data = None, filename = None,
                  ids = (), idrefs = (), filenames = ()):
    """Return a list of SAX event generated for a given XML source

    The xml source may either be a string with the actual XML, in which
    case it should be given as the keyword argument data or the name of
    an xml file given as the keyword argument filename
    """
    if filename is not None:
        data = open(filename).read()
    handler = SaxEventLister(ids = ids, idrefs = idrefs, filenames = filenames)
    parser = make_parser()
    parser.setContentHandler(handler)
    parser.setErrorHandler(ErrorHandler())
    parser.setFeature(xml.sax.handler.feature_namespaces, 1)

    #
    # see comment at the end of Thuban/Model/load.py
    #
    try:
        parser.setFeature(xml.sax.handler.feature_validation, 0)
        parser.setFeature(xml.sax.handler.feature_external_ges, 0)
        parser.setFeature(xml.sax.handler.feature_external_pes, 0)
    except SAXNotRecognizedException:
        pass

    inpsrc = xml.sax.InputSource()
    inpsrc.setByteStream(StringIO(data))
    parser.parse(inpsrc)

    return handler.eventlist


def print_summary_message():
    """Print a summary message about validation tests

    Currently simply print a message about pyRXP not being available if
    a test case's attempt to validate XML was ignored because of that.
    """
    if ValidationTest.validation_attempt_ignored:
        print "XML validation attempts ignored because pyRXP is not available"
