########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Server/Drivers/FtssInputSource.py,v 1.30 2005/03/22 17:36:57 mbrown Exp $
"""
A subclass of InputSource that provides access to repo resources.

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

import cStringIO

from Ft.Lib import Uri, Uuid
from Ft.Xml import InputSource, Domlette, Catalog
from Ft.Server import FTSS_URI_SCHEME
from Ft.Server.Common.ResourceTypes import ResourceType
from Ft.Server.Server import FtServerServerException, Error

import PathImp

FTSS_RESOLVER = Uri.FtUriResolver()
FTSS_RESOLVER.supportedSchemes.append(FTSS_URI_SCHEME)


class FtssInputSource(InputSource.InputSource):
    """
    Wraps content that streams from the local repository.

    The required uri argument given in the constructor must represent
    the stream's repo path in the form of a URI that uses the %s
    scheme. Although the URI will be resolved to absolute form using
    the base %s:/// if it is given as a relative (schemeless) URI that
    starts with '/', has an empty authority component, and has UTF-8
    based %%-escaping where necessary, it is preferable to just
    construct an FtssInputSource from the absolute URI that is
    returned by calling RepoPathToUri() on the desired path.

    The required driver argument given in the constructor must be a
    repo driver (FtssDriver) instance.
    """ % (FTSS_URI_SCHEME, FTSS_URI_SCHEME)

    FORBIDDEN_HINTS = ["REPO APPLYXSLT", 'STYLESHEET IMPORT', 'STYLESHEET INCLUDE']
    PERMITTED_HINTS = ["XSLT DOCUMENT FUNCTION", "EXTERNAL ENTITY"]
    #file:// forbidden until further security review
    FORBIDDEN_SCHEMES = ["file"]

    def __init__(self, stream, uri, driver, *v_args, **kw_args):
        # uri formats:
        #   (None or '')
        #   /absolute%20path
        #   //authority/absolute%20path
        #   relative%20path
        #   scheme://authority/absolute%20path
        #   scheme:opaque%20part%20or%20relpath
        #   scheme:/absolute%20path
        # 1. ensure that the uri was given
        if not uri:
            raise FtServerServerException(
                Error.RELATIVE_PATH_IN_FTSS_URI,
                uri=uri, ftssScheme=FTSS_URI_SCHEME)
        # 2. ensure that the path part of the uri is
        # absolute. if the uri has no scheme, assume the
        # correct one.
        origUri = uri[:]
        (scheme, authority, path) = Uri.SplitUriRef(uri)[0:3]
        if not scheme:
            if uri.startswith('/'):
                base = FTSS_URI_SCHEME+':///'
                uri = Uri.Absolutize(uri, base)
                scheme = FTSS_URI_SCHEME
            else:
                raise FtServerServerException(
                    Error.RELATIVE_PATH_IN_FTSS_URI,
                    uri=uri, ftssScheme=FTSS_URI_SCHEME)
        elif not path or path and not path.startswith('/'):
            raise FtServerServerException(
                Error.RELATIVE_PATH_IN_FTSS_URI,
                uri=uri, ftssScheme=FTSS_URI_SCHEME)
        # 3. uri is now absolute.
        # is scheme ok? (should be FTSS_URI_SCHEME)
        if scheme != FTSS_URI_SCHEME:
            raise FtServerServerException(
                Error.UNSUPPORTED_FTSS_URI_SCHEME,
                uri=uri, ftssScheme=FTSS_URI_SCHEME, scheme=scheme)
        # 4. authority ok? (should indicate a local repo)
        if authority:
            raise FtServerServerException(
                Error.NO_REMOTE_REPO_ACCESS, uri=uri)
        # 5. uri is good. convert path component to repo path,
        # in case we need it later.
        self._repo_path = unicode(Uri.PercentDecode(path.encode('ascii')),
                                  'utf-8')
        # 6. finish setting up instance
        self._driver = driver

        #7.  Force the factory to be a FtssInputSourceFactory
        kw_args['factory'] = _FtssInputSourceFactory(driver)

        InputSource.InputSource.__init__(self, stream, uri, *v_args, **kw_args)

        # if the regular InputSource constructor caused an encoding
        # to be set, unset it here, because for now, we don't support
        # external encoding declarations on repo resources, plus we
        # don't want 'text/xml' repo streams to be assumed us-ascii.
        self.encoding = None
        #print "%r for path %r created w/URI %r (from %r)" % (self, self._repo_path, uri, origUri)
        return

    def resolve(self, sysid, pubid=None, hint=None, ignoreErrors=0):
        """
        Resolves a system identifier (fragmentless URI reference) or a
        public identifier into a new input source. It is typically used
        when a URI reference is encountered in the original stream and
        needs to be resolved (e.g. to support an external entity
        reference, XInclude, xsl:include/import, document(), etc.).

        The hint parameter is used to give a hint as to what the
        resolution will be used for. It is generally used for security
        checks for the repository.

        If the ignoreErrors flag is set, an error during resolution
        (such as "file not found") will result in None being returned,
        rather than raising an exception.
        """
        sysid = self.resolveEntity(pubid, sysid)
        uri = self._normalize(sysid)
        stream = self._openStream(uri, ignoreErrors, hint)
        return self.clone(stream, uri, hint)

    def _openStream(self, uri, ignoreErrors=0, hint=None):
        """
        Returns a representation of a resource as a stream by
        resolving the given URI. If ignoreErrors is set, failure to
        obtain the stream will result in None being returned, rather
        than an exception (e.g. "file not found") being raised.

        Differs from the base class's version in that if the URI
        indicates a local repo resource, an attempt is made to obtain
        the stream directly, to avoid a possible deadlock. Any other
        URI is passed to the base class's _openStream().
        """
        #print "_openStream(%r) called on %r for URI %r" % (uri, self, self.uri)
        (scheme, authority, path) = Uri.SplitUriRef(uri)[0:3]
        if scheme == FTSS_URI_SCHEME:
            # URI is pointing to a repo resource
            if authority:
                raise FtServerServerException(
                    Error.NO_REMOTE_REPO_ACCESS, uri=uri)
            if not path or path and not path.startswith('/'):
                raise FtServerServerException(
                    Error.RELATIVE_PATH_IN_FTSS_URI,
                    uri=uri, ftssScheme=FTSS_URI_SCHEME)
            self._repo_path = unicode(Uri.PercentDecode(path.encode(
                                      'ascii')), 'utf-8')
            root = PathImp.CreateInitialPath('/', self._driver)
            path = root.normalize(uri[7:])  #Remove Scheme
            if self._driver.hasResource(path):
                # in repo
                #print "returning repo doc %s" % self._repo_path
                if self._driver.getType(path) != ResourceType.RDF_DOCUMENT:
                    # Fast track; bypass the repository and get the content
                    # directly from the driver
                    content = self._driver.fetchResource(path)
                else:
                    # WARNING - BIG UGLY HACK!!!!!!!!
                    # Generated content; use a repository to get the resource
                    # object on which to call getContent().
                    from Ft.Server.Server.SCore import RepositoryImp
                    repo = RepositoryImp.RepositoryImp(root, self._driver, "")
                    content = repo._fetchResource(path).getContent()
                return cStringIO.StringIO(content)
            else:
                # not in repo; don't try anywhere else
                #print "repo doc %s not found" % self._repo_path
                if ignoreErrors:
                    return None
                raise FtServerServerException(
                    Error.RESOURCE_NOT_FOUND,
                    uri=uri)
        else:
            # non-repo
            #print "non-repo URI, so calling InputSource._openStream(%r)" % uri
            return InputSource.InputSource._openStream(self, uri)


    def clone(self, stream, uri=None, hint=None):
        """
        Clones this input source, creating a new instance with
        the known params.
        """
        #print "cloning %r with URI %r; new uri given was %r" % (self, self.uri, uri)
        if uri is None:
            uri = self.uri
        if stream is None:
            return InputSource.NullInputSource(uri)
        try:
            return FtssInputSource(stream, uri, self._driver,
                                   processIncludes=self.processIncludes,
                                   stripElements=self.stripElements,
                                   resolver=self._resolver)
        except FtServerServerException, e:
            if e.errorCode == Error.UNSUPPORTED_FTSS_URI_SCHEME:
                if hint in self.FORBIDDEN_HINTS:
                    raise
                if e.params["scheme"] in self.FORBIDDEN_SCHEMES:
                    raise
                if hint not in self.PERMITTED_HINTS:
                    classname = self.__class__.__name__
                    print ("%s resolution attempted with hint"
                           " %r.  This will not be allowed until this hint"
                           " is added to %s.PERMITTED_HINTS") % (classname,
                                                                 hint,
                                                                 classname)
                    raise
                return InputSource.InputSource(
                    stream, uri, processIncludes=self.processIncludes,
                    stripElements=self.stripElements,
                    resolver=self._resolver)
            raise

    def __getstate__(self):
        #Must not pickle the driver
        state = InputSource.InputSource.__getstate__(self)
        state['_driver'] = None
        return state


class _FtssInputSourceFactory(InputSource.InputSourceFactory):

    def __init__(self, driver, *v_args, **kw_args):
        kw_args['inputSourceClass'] = FtssInputSource
        kw_args['resolver'] = FTSS_RESOLVER
        InputSource.InputSourceFactory.__init__(self, *v_args, **kw_args)
        #self._fallbackFactory = InputSource.DefaultFactory
        self._driver = driver
        self.catalog = Catalog.GetDefaultCatalog('ftss-default.cat')
        return

    def fromStream(self, stream, uri=None, *v_args, **kw_args):
        #Must be either in the first v_arg, or in the kw_args (or have self._driver set)
        if len(v_args) == 0 and not kw_args.has_key('driver'):
            if self._driver is None:
                raise RuntimeError("You must specify a driver when using the global input source factory.")
            kw_args['driver'] = self._driver
        #try:
        #    return apply(InputSource.InputSourceFactory.fromStream,
        #                 (self, stream, uri) + v_args, kw_args)
        #except FtServerServerException, e:
        #    if e.errorCode == Error.UNSUPPORTED_FTSS_URI_SCHEME:
        #        return apply(self._fallbackFactory.fromStream,
        #                     (self, stream, uri) + v_args, kw_args)
        if 'catalog' not in kw_args: kw_args['catalog'] = self.catalog
        return InputSource.InputSourceFactory.fromStream(self, stream, uri,
                                                         *v_args, **kw_args)


# Set up defaults
FtssInputSourceFactory = _FtssInputSourceFactory(None)

ValidatingReader = Domlette.ValidatingReaderBase(FtssInputSourceFactory)
NonvalidatingReader = Domlette.NonvalidatingReaderBase(FtssInputSourceFactory)
NoExtDtdReader = Domlette.NoExtDtdReaderBase(FtssInputSourceFactory)

