#!/usr/bin/env python
#
# Copyright 2007 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#


"""Dispatch configuration tools.

Library for parsing dispatch.yaml files and working with these in memory.
"""

import re

import six

from google.appengine.api import appinfo
from google.appengine.api import validation
from google.appengine.api import yaml_builder
from google.appengine.api import yaml_listener
from google.appengine.api import yaml_object








_URL_SPLITTER_RE = re.compile(r'^([^/]+)(/.*)$')






_URL_HOST_EXACT_PATTERN_RE = re.compile(r"""
# 0 or more . terminated hostname segments (may not start or end in -).
^([a-z0-9]([a-z0-9\-]*[a-z0-9])*\.)*
# followed by a host name segment.
([a-z0-9]([a-z0-9\-]*[a-z0-9])*)$""", re.VERBOSE)

_URL_IP_V4_ADDR_RE = re.compile(r"""
#4 1-3 digit numbers separated by .
^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$""", re.VERBOSE)



_URL_HOST_SUFFIX_PATTERN_RE = re.compile(r"""
# Single star or
^([*]|
# Host prefix with no .,  Ex '*-a' or
[*][a-z0-9\-]*[a-z0-9]|
# Host prefix with ., Ex '*-a.b-c.d'
[*](\.|[a-z0-9\-]*[a-z0-9]\.)([a-z0-9]([a-z0-9\-]*[a-z0-9])*\.)*
([a-z0-9]([a-z0-9\-]*[a-z0-9])*))$
""", re.VERBOSE)

APPLICATION = 'application'
DISPATCH = 'dispatch'
URL = 'url'
MODULE = 'module'
SERVICE = 'service'


class Error(Exception):
  """Base class for errors in this module."""


class MalformedDispatchConfigurationError(Error):
  """Configuration file for dispatch is malformed."""


class DispatchEntryURLValidator(validation.Validator):
  """Validater for URL patterns."""

  def Validate(self, value, unused_key=None):
    """Validates an URL pattern."""
    if value is None:
      raise validation.MissingAttribute('url must be specified')
    if not isinstance(value, six.string_types):
      raise validation.ValidationError('url must be a string, not \'%r\'' %
                                       type(value))

    url_holder = ParsedURL(value)
    if url_holder.host_exact:
      _ValidateMatch(_URL_HOST_EXACT_PATTERN_RE, url_holder.host,
                     'invalid host_pattern \'%s\'' % url_holder.host)


      _ValidateNotIpV4Address(url_holder.host)
    else:
      _ValidateMatch(_URL_HOST_SUFFIX_PATTERN_RE, url_holder.host_pattern,
                     'invalid host_pattern \'%s\'' % url_holder.host_pattern)



    return value


class ParsedURL(object):
  """Dispath Entry URL holder class.

  Attributes:
    host_pattern: The host pattern component of the URL pattern.
    host_exact: True if the host pattern does not start with a *.
    host: host_pattern  with any leading * removed.
    path_pattern: The path pattern component of the URL pattern.
    path_exact: True if the path_pattern does not end with a *.
    path: path_pattern with any trailing * removed.
  """

  def __init__(self, url_pattern):
    """Initializes this ParsedURL with an URL pattern value.

    Args:
      url_pattern: An URL pattern that conforms to the regular expression
          '^([^/]+)(/.*)$'.

    Raises:
      validation.ValidationError: When url_pattern does not match the required
          regular expression.
    """
    split_matcher = _ValidateMatch(_URL_SPLITTER_RE, url_pattern,
                                   'invalid url \'%s\'' % url_pattern)
    self.host_pattern, self.path_pattern = split_matcher.groups()
    if self.host_pattern.startswith('*'):
      self.host_exact = False
      self.host = self.host_pattern[1:]
    else:
      self.host_exact = True
      self.host = self.host_pattern

    if self.path_pattern.endswith('*'):
      self.path_exact = False
      self.path = self.path_pattern[:-1]
    else:
      self.path_exact = True
      self.path = self.path_pattern


def _ValidateMatch(regex, value, message):
  """Validate value matches regex."""
  matcher = regex.match(value)
  if not matcher:
    raise validation.ValidationError(message)
  return matcher


def _ValidateNotIpV4Address(host):
  """Validate host is not an IPV4 address."""
  matcher = _URL_IP_V4_ADDR_RE.match(host)
  if matcher and sum(1 for x in matcher.groups() if int(x) <= 255) == 4:
    raise validation.ValidationError('Host may not match an ipv4 address \'%s\''
                                     % host)
  return matcher


class DispatchEntry(validation.Validated):
  """A Dispatch entry describes a mapping from a URL pattern to a module."""
  ATTRIBUTES = {
      URL: DispatchEntryURLValidator(),
      SERVICE: validation.Preferred(MODULE, appinfo.MODULE_ID_RE_STRING),
      MODULE: validation.Deprecated(SERVICE, appinfo.MODULE_ID_RE_STRING),
  }


class DispatchInfoExternal(validation.Validated):
  """Describes the format of a dispatch.yaml file."""
  ATTRIBUTES = {
      APPLICATION: validation.Optional(appinfo.APPLICATION_RE_STRING),
      DISPATCH: validation.Optional(validation.Repeated(DispatchEntry)),
  }


def LoadSingleDispatch(dispatch_info, open_fn=None):
  """Load a dispatch.yaml file or string and return a DispatchInfoExternal.

  Args:
    dispatch_info: The contents of a dispatch.yaml file as a string, or an open
      file object.
    open_fn: Function for opening files. Unused here, needed to provide
      a polymorphic API used by appcfg.py yaml parsing.

  Returns:
    A DispatchInfoExternal instance which represents the contents of the parsed
      yaml file.

  Raises:
    MalformedDispatchConfigurationError: The yaml file contains multiple
      dispatch sections or is missing a required value.
    yaml_errors.EventError: An error occured while parsing the yaml file.
  """
  del open_fn
  builder = yaml_object.ObjectBuilder(DispatchInfoExternal)
  handler = yaml_builder.BuilderHandler(builder)
  listener = yaml_listener.EventListener(handler)
  listener.Parse(dispatch_info)

  parsed_yaml = handler.GetResults()
  if not parsed_yaml:
    return DispatchInfoExternal()
  if len(parsed_yaml) > 1:
    raise MalformedDispatchConfigurationError('Multiple dispatch: sections '
                                              'in configuration.')



  dispatch_info_external = parsed_yaml[0]
  dispatch_info_external.CheckInitialized()
  return dispatch_info_external
