# -*- test-case-name: txweb2.test.test_server,twext.web2.test.test_resource -*-
##
# Copyright (c) 2001-2007 Twisted Matrix Laboratories.
# Copyright (c) 2010-2017 Apple Inc. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
##

"""
I hold the lowest-level L{Resource} class and related mix-in classes.
"""

# System Imports
from zope.interface import implements

from twisted.internet.defer import inlineCallbacks, returnValue

from txweb2 import iweb, http, server, responsecode

from twisted.internet.defer import maybeDeferred


class RenderMixin(object):
    """
    Mix-in class for L{iweb.IResource} which provides a dispatch mechanism for
    handling HTTP methods.
    """

    def allowedMethods(self):
        """
        @return: A tuple of HTTP methods that are allowed to be invoked on this
            resource.
        """
        if not hasattr(self, "_allowed_methods"):
            self._allowed_methods = tuple(
                name[5:] for name in dir(self)
                if name.startswith('http_') and getattr(self, name) is not None
            )
        return self._allowed_methods

    def checkPreconditions(self, request):
        """
        Checks all preconditions imposed by this resource upon a request made
        against it.
        @param request: the request to process.
        @raise http.HTTPError: if any precondition fails.
        @return: C{None} or a deferred whose callback value is C{request}.
        """
        #
        # http.checkPreconditions() gets called by the server after every
        # GET or HEAD request.
        #
        # For other methods, we need to know to bail out before request
        # processing, especially for methods that modify server state (eg.
        # PUT).
        # We also would like to do so even for methods that don't, if those
        # methods might be expensive to process.  We're assuming that GET and
        # HEAD are not expensive.
        #
        if request.method not in ("GET", "HEAD"):
            http.checkPreconditions(request)

        # Check per-method preconditions
        method = getattr(self, "preconditions_" + request.method, None)
        if method:
            return method(request)

    @inlineCallbacks
    def renderHTTP(self, request):
        """
        See L{iweb.IResource.renderHTTP}.

        This implementation will dispatch the given C{request} to another
        method of C{self} named C{http_}METHOD, where METHOD is the HTTP method
        used by C{request} (eg. C{http_GET}, C{http_POST}, etc.).

        Generally, a subclass should implement those methods instead of
        overriding this one.

        C{http_*} methods are expected provide the same interface and return
        the same results as L{iweb.IResource}C{.renderHTTP} (and therefore this
        method).

        C{etag} and C{last-modified} are added to the response returned by the
        C{http_*} header, if known.

        If an appropriate C{http_*} method is not found, a
        L{responsecode.NOT_ALLOWED}-status response is returned, with an
        appropriate C{allow} header.

        @param request: the request to process.
        @return: an object adaptable to L{iweb.IResponse}.
        """
        method = getattr(self, "http_" + request.method, None)
        if method is None:
            response = http.Response(responsecode.NOT_ALLOWED)
            response.headers.setHeader("allow", self.allowedMethods())
            returnValue(response)

        yield self.checkPreconditions(request)
        result = maybeDeferred(method, request)
        result.addErrback(self.methodRaisedException)
        returnValue((yield result))

    def methodRaisedException(self, failure):
        """
        An C{http_METHOD} method raised an exception; this is an errback for
        that exception.  By default, simply propagate the error up; subclasses
        may override this for top-level exception handling.
        """
        return failure

    def http_OPTIONS(self, request):
        """
        Respond to a OPTIONS request.
        @param request: the request to process.
        @return: an object adaptable to L{iweb.IResponse}.
        """
        response = http.Response(responsecode.OK)
        response.headers.setHeader("allow", self.allowedMethods())
        return response

#    def http_TRACE(self, request):
#        """
#        Respond to a TRACE request.
#        @param request: the request to process.
#        @return: an object adaptable to L{iweb.IResponse}.
#        """
#        return server.doTrace(request)

    def http_HEAD(self, request):
        """
        Respond to a HEAD request.
        @param request: the request to process.
        @return: an object adaptable to L{iweb.IResponse}.
        """
        return self.http_GET(request)

    def http_GET(self, request):
        """
        Respond to a GET request.

        This implementation validates that the request body is empty and then
        dispatches the given C{request} to L{render} and returns its result.

        @param request: the request to process.
        @return: an object adaptable to L{iweb.IResponse}.
        """
        if request.stream.length != 0:
            return responsecode.REQUEST_ENTITY_TOO_LARGE

        return self.render(request)

    def render(self, request):
        """
        Subclasses should implement this method to do page rendering.
        See L{http_GET}.
        @param request: the request to process.
        @return: an object adaptable to L{iweb.IResponse}.
        """
        raise NotImplementedError("Subclass must implement render method.")


class Resource(RenderMixin):
    """
    An L{iweb.IResource} implementation with some convenient mechanisms for
    locating children.
    """
    implements(iweb.IResource)

    addSlash = False

    def locateChild(self, request, segments):
        """
        Locates a child resource of this resource.
        @param request: the request to process.
        @param segments: a sequence of URL path segments.
        @return: a tuple of C{(child, segments)} containing the child
        of this resource which matches one or more of the given C{segments} in
        sequence, and a list of remaining segments.
        """
        w = self.getChild(segments[0])

        if w:
            r = iweb.IResource(w, None)
            if r:
                return r, segments[1:]
            return w(request), segments[1:]

        factory = getattr(self, 'childFactory', None)
        if factory is not None:
            r = factory(request, segments[0])
            if r:
                return r, segments[1:]

        return None, []

    def child_(self, request):
        """
        This method locates a child with a trailing C{"/"} in the URL.
        @param request: the request to process.
        """
        if self.addSlash and len(request.postpath) == 1:
            return self
        return None

    def getChild(self, path):
        """
        Get a static child - when registered using L{putChild}.

        @param path: the name of the child to get
        @type path: C{str}

        @return: the child or C{None} if not present
        @rtype: L{iweb.IResource}
        """
        return getattr(self, 'child_%s' % (path,), None)

    def putChild(self, path, child):
        """
        Register a static child.

        This implementation registers children by assigning them to attributes
        with a C{child_} prefix.  C{resource.putChild("foo", child)} is
        therefore same as C{o.child_foo = child}.

        @param path: the name of the child to register.  You almost certainly
            don't want C{"/"} in C{path}. If you want to add a "directory"
            resource (e.g. C{/foo/}) specify C{path} as C{""}.
        @param child: an object adaptable to L{iweb.IResource}.
        """
        setattr(self, 'child_%s' % (path,), child)

    def http_GET(self, request):
        if self.addSlash and request.prepath[-1] != '':
            # If this is a directory-ish resource...
            return http.RedirectResponse(
                request.unparseURL(path=request.path + '/')
            )

        return super(Resource, self).http_GET(request)


class PostableResource(Resource):
    """
    A L{Resource} capable of handling the POST request method.

    @cvar maxMem: maximum memory used during the parsing of the data.
    @type maxMem: C{int}
    @cvar maxFields: maximum number of form fields allowed.
    @type maxFields: C{int}
    @cvar maxSize: maximum size of the whole post allowed.
    @type maxSize: C{int}
    """
    maxMem = 100 * 1024
    maxFields = 1024
    maxSize = 10 * 1024 * 1024

    def http_POST(self, request):
        """
        Respond to a POST request.
        Reads and parses the incoming body data then calls L{render}.

        @param request: the request to process.
        @return: an object adaptable to L{iweb.IResponse}.
        """
        return server.parsePOSTData(
            request, self.maxMem, self.maxFields, self.maxSize
        ).addCallback(lambda res: self.render(request))


class LeafResource(RenderMixin):
    """
    A L{Resource} with no children.
    """
    implements(iweb.IResource)

    def locateChild(self, request, segments):
        return self, server.StopTraversal


class RedirectResource(LeafResource):
    """
    A L{LeafResource} which always performs a redirect.
    """
    implements(iweb.IResource)

    def __init__(self, *args, **kwargs):
        """
        Parameters are URL components and are the same as those for
        L{urlparse.urlunparse}.  URL components which are not specified will
        default to the corresponding component of the URL of the request being
        redirected.
        """
        self._args = args
        self._kwargs = kwargs

    def renderHTTP(self, request):
        return http.RedirectResponse(
            request.unparseURL(*self._args, **self._kwargs)
        )


class WrapperResource(object):
    """
    An L{iweb.IResource} implementation which wraps a L{RenderMixin} instance
    and provides a hook in which a subclass can implement logic that is called
    before request processing on the contained L{Resource}.
    """
    implements(iweb.IResource)

    def __init__(self, resource):
        self.resource = resource

    def hook(self, request):
        """
        Override this method in order to do something before passing control on
        to the wrapped resource's C{renderHTTP} and C{locateChild} methods.
        @return: None or a L{Deferred}.  If a deferred object is
            returned, it's value is ignored, but C{renderHTTP} and
            C{locateChild} are chained onto the deferred as callbacks.
        """
        raise NotImplementedError()

    def locateChild(self, request, segments):
        x = self.hook(request)
        if x is not None:
            return x.addCallback(lambda data: (self.resource, segments))
        return self.resource, segments

    def renderHTTP(self, request):
        x = self.hook(request)
        if x is not None:
            return x.addCallback(lambda data: self.resource)
        return self.resource

    def getChild(self, name):
        return self.resource.getChild(name)


__all__ = [
    'RenderMixin', 'Resource',
    'PostableResource', 'LeafResource', 'WrapperResource'
]
