################################################################################
#  Licensed to the Apache Software Foundation (ASF) under one
#  or more contributor license agreements.  See the NOTICE file
#  distributed with this work for additional information
#  regarding copyright ownership.  The ASF licenses this file
#  to you 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.
################################################################################
import sys
from abc import ABCMeta
from collections import OrderedDict

from py4j.java_gateway import get_method
from typing import Dict, Union

from pyflink.java_gateway import get_gateway
from pyflink.table.table_schema import TableSchema
from pyflink.table.types import _to_java_type, DataType

__all__ = [
    'Rowtime',
    'Schema',
    'OldCsv',
    'FileSystem',
    'Kafka',
    'Elasticsearch',
    'Csv',
    'Avro',
    'Json',
    'ConnectTableDescriptor',
    'StreamTableDescriptor',
    'CustomConnectorDescriptor',
    'CustomFormatDescriptor'
]


class Descriptor(object, metaclass=ABCMeta):
    """
    Base class of the descriptors that adds a set of string-based, normalized properties for
    describing DDL information.

    Typical characteristics of a descriptor are:
    - descriptors have a default constructor
    - descriptors themselves contain very little logic
    - corresponding validators validate the correctness (goal: have a single point of validation)

    A descriptor is similar to a builder in a builder pattern, thus, mutable for building
    properties.
    """

    def __init__(self, j_descriptor):
        self._j_descriptor = j_descriptor

    def to_properties(self) -> Dict:
        """
        Converts this descriptor into a dict of properties.

        :return: Dict object contains all of current properties.
        """
        return dict(self._j_descriptor.toProperties())


class Rowtime(Descriptor):
    """
    Rowtime descriptor for describing an event time attribute in the schema.
    """

    def __init__(self):
        gateway = get_gateway()
        self._j_rowtime = gateway.jvm.Rowtime()
        super(Rowtime, self).__init__(self._j_rowtime)

    def timestamps_from_field(self, field_name: str):
        """
        Sets a built-in timestamp extractor that converts an existing LONG or TIMESTAMP field into
        the rowtime attribute.

        :param field_name: The field to convert into a rowtime attribute.
        :return: This rowtime descriptor.
        """
        self._j_rowtime = self._j_rowtime.timestampsFromField(field_name)
        return self

    def timestamps_from_source(self) -> 'Rowtime':
        """
        Sets a built-in timestamp extractor that converts the assigned timestamps from a DataStream
        API record into the rowtime attribute and thus preserves the assigned timestamps from the
        source.

        .. note::

            This extractor only works in streaming environments.

        :return: This rowtime descriptor.
        """
        self._j_rowtime = self._j_rowtime.timestampsFromSource()
        return self

    def timestamps_from_extractor(self, extractor: str) -> 'Rowtime':
        """
        Sets a custom timestamp extractor to be used for the rowtime attribute.

        :param extractor: The java fully-qualified class name of the TimestampExtractor to extract
                          the rowtime attribute from the physical type. The TimestampExtractor must
                          have a public no-argument constructor and can be founded by
                          in current Java classloader.
        :return: This rowtime descriptor.
        """
        gateway = get_gateway()
        self._j_rowtime = self._j_rowtime.timestampsFromExtractor(
            gateway.jvm.Thread.currentThread().getContextClassLoader().loadClass(extractor)
                   .newInstance())
        return self

    def watermarks_periodic_ascending(self) -> 'Rowtime':
        """
        Sets a built-in watermark strategy for ascending rowtime attributes.

        Emits a watermark of the maximum observed timestamp so far minus 1. Rows that have a
        timestamp equal to the max timestamp are not late.

        :return: This rowtime descriptor.
        """
        self._j_rowtime = self._j_rowtime.watermarksPeriodicAscending()
        return self

    def watermarks_periodic_bounded(self, delay: int) -> 'Rowtime':
        """
        Sets a built-in watermark strategy for rowtime attributes which are out-of-order by a
        bounded time interval.

        Emits watermarks which are the maximum observed timestamp minus the specified delay.

        :param delay: Delay in milliseconds.
        :return: This rowtime descriptor.
        """
        self._j_rowtime = self._j_rowtime.watermarksPeriodicBounded(delay)
        return self

    def watermarks_from_source(self) -> 'Rowtime':
        """
        Sets a built-in watermark strategy which indicates the watermarks should be preserved from
        the underlying DataStream API and thus preserves the assigned watermarks from the source.

        :return: This rowtime descriptor.
        """
        self._j_rowtime = self._j_rowtime.watermarksFromSource()
        return self

    def watermarks_from_strategy(self, strategy: str) -> 'Rowtime':
        """
        Sets a custom watermark strategy to be used for the rowtime attribute.

        :param strategy: The java fully-qualified class name of the WatermarkStrategy. The
                         WatermarkStrategy must have a public no-argument constructor and can be
                         founded by in current Java classloader.
        :return: This rowtime descriptor.
        """
        gateway = get_gateway()
        self._j_rowtime = self._j_rowtime.watermarksFromStrategy(
            gateway.jvm.Thread.currentThread().getContextClassLoader().loadClass(strategy)
                   .newInstance())
        return self


class Schema(Descriptor):
    """
    Describes a schema of a table.

    .. note::

        Field names are matched by the exact name by default (case sensitive).
    """

    def __init__(self, schema=None, fields=None, rowtime=None):
        """
        Constructor of Schema descriptor.

        :param schema: The :class:`TableSchema` object.
        :param fields: Dict of fields with the field name and the data type or type string stored.
        :param rowtime: A :class:`RowTime` that Specifies the previously defined field as an
                        event-time attribute.
        """
        gateway = get_gateway()
        self._j_schema = gateway.jvm.org.apache.flink.table.descriptors.Schema()
        super(Schema, self).__init__(self._j_schema)

        if schema is not None:
            self.schema(schema)

        if fields is not None:
            self.fields(fields)

        if rowtime is not None:
            self.rowtime(rowtime)

    def schema(self, table_schema: 'TableSchema') -> 'Schema':
        """
        Sets the schema with field names and the types. Required.

        This method overwrites existing fields added with
        :func:`~pyflink.table.descriptors.Schema.field`.

        :param table_schema: The :class:`TableSchema` object.
        :return: This schema object.
        """
        self._j_schema = self._j_schema.schema(table_schema._j_table_schema)
        return self

    def field(self, field_name: str, field_type: Union[DataType, str]) -> 'Schema':
        """
        Adds a field with the field name and the data type or type string. Required.
        This method can be called multiple times. The call order of this method defines
        also the order of the fields in a row. Here is a document that introduces the type strings:
        https://ci.apache.org/projects/flink/flink-docs-stable/dev/table/connect.html#type-strings

        :param field_name: The field name.
        :param field_type: The data type or type string of the field.
        :return: This schema object.
        """
        if isinstance(field_type, str):
            self._j_schema = self._j_schema.field(field_name, field_type)
        else:
            self._j_schema = self._j_schema.field(field_name, _to_java_type(field_type))
        return self

    def fields(self, fields: Dict[str, Union[DataType, str]]) -> 'Schema':
        """
        Adds a set of fields with the field name and the data type or type string stored in a
        list.

        :param fields: Dict of fields with the field name and the data type or type string
                       stored.
                       E.g, [('int_field', DataTypes.INT()), ('string_field', DataTypes.STRING())].
        :return: This schema object.

        .. versionadded:: 1.11.0
        """
        if sys.version_info[:2] <= (3, 5) and not isinstance(fields, OrderedDict):
            raise TypeError("Must use OrderedDict type in python3.5 or older version to key the "
                            "schema in insert order.")
        elif sys.version_info[:2] > (3, 5) and not isinstance(fields, (OrderedDict, dict)):
            raise TypeError("fields must be stored in a dict or OrderedDict")

        for field_name, field_type in fields.items():
            self.field(field_name=field_name, field_type=field_type)
        return self

    def from_origin_field(self, origin_field_name: str) -> 'Schema':
        """
        Specifies the origin of the previously defined field. The origin field is defined by a
        connector or format.

        E.g. field("myString", Types.STRING).from_origin_field("CSV_MY_STRING")

        .. note::

            Field names are matched by the exact name by default (case sensitive).

        :param origin_field_name: The origin field name.
        :return: This schema object.
        """
        self._j_schema = get_method(self._j_schema, "from")(origin_field_name)
        return self

    def proctime(self) -> 'Schema':
        """
        Specifies the previously defined field as a processing-time attribute.

        E.g. field("proctime", Types.SQL_TIMESTAMP_LTZ).proctime()

        :return: This schema object.
        """
        self._j_schema = self._j_schema.proctime()
        return self

    def rowtime(self, rowtime: Rowtime) -> 'Schema':
        """
        Specifies the previously defined field as an event-time attribute.

        E.g. field("rowtime", Types.SQL_TIMESTAMP).rowtime(...)

        :param rowtime: A :class:`RowTime`.
        :return: This schema object.
        """
        self._j_schema = self._j_schema.rowtime(rowtime._j_rowtime)
        return self


class FormatDescriptor(Descriptor, metaclass=ABCMeta):
    """
    Describes the format of data.
    """

    def __init__(self, j_format_descriptor):
        self._j_format_descriptor = j_format_descriptor
        super(FormatDescriptor, self).__init__(self._j_format_descriptor)


class OldCsv(FormatDescriptor):
    """
    Format descriptor for comma-separated values (CSV).

    .. note::

        This descriptor describes Flink's non-standard CSV table source/sink. In the future, the
        descriptor will be replaced by a proper RFC-compliant version. Use the RFC-compliant `Csv`
        format in the dedicated `flink-formats/flink-csv` module instead when writing to Kafka. Use
        the old one for stream/batch filesystem operations for now.

    .. note::

        Deprecated: use the RFC-compliant `Csv` format instead when writing to Kafka.
    """

    def __init__(self, schema=None, field_delimiter=None, line_delimiter=None, quote_character=None,
                 comment_prefix=None, ignore_parse_errors=False, ignore_first_line=False):
        """
        Constructor of OldCsv format descriptor.

        :param schema: Data type from :class:`DataTypes` that describes the schema.
        :param field_delimiter: The field delimiter character.
        :param line_delimiter: The line delimiter.
        :param quote_character: The quote character.
        :param comment_prefix: The prefix to indicate comments.
        :param ignore_parse_errors: Skip records with parse error instead to fail. Throw an
                                    exception by default.
        :param ignore_first_line: Ignore the first line. Not skip the first line by default.
        """
        gateway = get_gateway()
        self._j_csv = gateway.jvm.OldCsv()
        super(OldCsv, self).__init__(self._j_csv)

        if schema is not None:
            self.schema(schema)

        if field_delimiter is not None:
            self.field_delimiter(field_delimiter)

        if line_delimiter is not None:
            self.line_delimiter(line_delimiter)

        if quote_character is not None:
            self.quote_character(quote_character)

        if comment_prefix is not None:
            self.comment_prefix(comment_prefix)

        if ignore_parse_errors:
            self.ignore_parse_errors()

        if ignore_first_line:
            self.ignore_first_line()

    def field_delimiter(self, delimiter: str) -> 'OldCsv':
        """
        Sets the field delimiter, "," by default.

        :param delimiter: The field delimiter.
        :return: This :class:`OldCsv` object.
        """
        self._j_csv = self._j_csv.fieldDelimiter(delimiter)
        return self

    def line_delimiter(self, delimiter: str) -> 'OldCsv':
        r"""
        Sets the line delimiter, "\\n" by default.

        :param delimiter: The line delimiter.
        :return: This :class:`OldCsv` object.
        """
        self._j_csv = self._j_csv.lineDelimiter(delimiter)
        return self

    def schema(self, table_schema: 'TableSchema') -> 'OldCsv':
        """
        Sets the schema with field names and the types. Required.

        This method overwrites existing fields added with
        :func:`~pyflink.table.descriptors.OldCsv.field`.

        :param table_schema: The :class:`TableSchema` object.
        :return: This :class:`OldCsv` object.
        """
        self._j_csv = self._j_csv.schema(table_schema._j_table_schema)
        return self

    def field(self, field_name: str, field_type: Union[DataType, str]) -> 'OldCsv':
        """
        Adds a format field with the field name and the data type or type string. Required.
        This method can be called multiple times. The call order of this method defines
        also the order of the fields in the format.

        :param field_name: The field name.
        :param field_type: The data type or type string of the field.
        :return: This :class:`OldCsv` object.
        """
        if isinstance(field_type, str):
            self._j_csv = self._j_csv.field(field_name, field_type)
        else:
            self._j_csv = self._j_csv.field(field_name, _to_java_type(field_type))
        return self

    def quote_character(self, quote_character: str) -> 'OldCsv':
        """
        Sets a quote character for String values, null by default.

        :param quote_character: The quote character.
        :return: This :class:`OldCsv` object.
        """
        self._j_csv = self._j_csv.quoteCharacter(quote_character)
        return self

    def comment_prefix(self, prefix: str) -> 'OldCsv':
        """
        Sets a prefix to indicate comments, null by default.

        :param prefix: The prefix to indicate comments.
        :return: This :class:`OldCsv` object.
        """
        self._j_csv = self._j_csv.commentPrefix(prefix)
        return self

    def ignore_parse_errors(self) -> 'OldCsv':
        """
        Skip records with parse error instead to fail. Throw an exception by default.

        :return: This :class:`OldCsv` object.
        """
        self._j_csv = self._j_csv.ignoreParseErrors()
        return self

    def ignore_first_line(self) -> 'OldCsv':
        """
        Ignore the first line. Not skip the first line by default.

        :return: This :class:`OldCsv` object.
        """
        self._j_csv = self._j_csv.ignoreFirstLine()
        return self


class Csv(FormatDescriptor):
    """
    Format descriptor for comma-separated values (CSV).

    This descriptor aims to comply with RFC-4180 ("Common Format and MIME Type for
    Comma-Separated Values (CSV) Files") proposed by the Internet Engineering Task Force (IETF).

    .. note::

        This descriptor does not describe Flink's old non-standard CSV table
        source/sink. Currently, this descriptor can be used when writing to Kafka. The old one is
        still available as :class:`OldCsv` for stream/batch filesystem operations.
    """

    def __init__(self, schema=None, field_delimiter=None, line_delimiter=None, quote_character=None,
                 allow_comments=False, ignore_parse_errors=False, array_element_delimiter=None,
                 escape_character=None, null_literal=None):
        """
        Constructor of Csv format descriptor.

        :param schema: Data type from :class:`DataTypes` that describes the schema.
        :param field_delimiter: The field delimiter character.
        :param line_delimiter: The line delimiter.
        :param quote_character: The quote character.
        :param allow_comments: Whether Ignores comment lines that start with '#' (disabled by
                               default).
        :param ignore_parse_errors: Whether Skip records with parse error instead to fail. Throw an
                                    exception by default.
        :param array_element_delimiter: The array element delimiter.
        :param escape_character: Escaping character (e.g. backslash).
        :param null_literal: The null literal string.
        """
        gateway = get_gateway()
        self._j_csv = gateway.jvm.Csv()
        super(Csv, self).__init__(self._j_csv)

        if schema is not None:
            self.schema(schema)

        if field_delimiter is not None:
            self.field_delimiter(field_delimiter)

        if line_delimiter is not None:
            self.line_delimiter(line_delimiter)

        if quote_character is not None:
            self.quote_character(quote_character)

        if allow_comments:
            self.allow_comments()

        if ignore_parse_errors:
            self.ignore_parse_errors()

        if array_element_delimiter is not None:
            self.array_element_delimiter(array_element_delimiter)

        if escape_character is not None:
            self.escape_character(escape_character)

        if null_literal is not None:
            self.null_literal(null_literal)

    def field_delimiter(self, delimiter: str) -> 'Csv':
        """
        Sets the field delimiter character (',' by default).

        :param delimiter: The field delimiter character.
        :return: This :class:`Csv` object.
        """
        if not isinstance(delimiter, str) or len(delimiter) != 1:
            raise TypeError("Only one-character string is supported!")
        self._j_csv = self._j_csv.fieldDelimiter(delimiter)
        return self

    def line_delimiter(self, delimiter: str) -> 'Csv':
        r"""
        Sets the line delimiter ("\\n" by default; otherwise "\\r" or "\\r\\n" are allowed).

        :param delimiter: The line delimiter.
        :return: This :class:`Csv` object.
        """
        self._j_csv = self._j_csv.lineDelimiter(delimiter)
        return self

    def quote_character(self, quote_character: str) -> 'Csv':
        """
        Sets the field delimiter character (',' by default).

        :param quote_character: The quote character.
        :return: This :class:`Csv` object.
        """
        if not isinstance(quote_character, str) or len(quote_character) != 1:
            raise TypeError("Only one-character string is supported!")
        self._j_csv = self._j_csv.quoteCharacter(quote_character)
        return self

    def allow_comments(self) -> 'Csv':
        """
        Ignores comment lines that start with '#' (disabled by default). If enabled, make sure to
        also ignore parse errors to allow empty rows.

        :return: This :class:`Csv` object.
        """
        self._j_csv = self._j_csv.allowComments()
        return self

    def ignore_parse_errors(self) -> 'Csv':
        """
        Skip records with parse error instead to fail. Throw an exception by default.

        :return: This :class:`Csv` object.
        """
        self._j_csv = self._j_csv.ignoreParseErrors()
        return self

    def array_element_delimiter(self, delimiter: str) -> 'Csv':
        """
        Sets the array element delimiter string for separating array or row element
        values (";" by default).

        :param delimiter: The array element delimiter.
        :return: This :class:`Csv` object.
        """
        self._j_csv = self._j_csv.arrayElementDelimiter(delimiter)
        return self

    def escape_character(self, escape_character: str) -> 'Csv':
        """
        Sets the escape character for escaping values (disabled by default).

        :param escape_character: Escaping character (e.g. backslash).
        :return: This :class:`Csv` object.
        """
        if not isinstance(escape_character, str) or len(escape_character) != 1:
            raise TypeError("Only one-character string is supported!")
        self._j_csv = self._j_csv.escapeCharacter(escape_character)
        return self

    def null_literal(self, null_literal: str) -> 'Csv':
        """
        Sets the null literal string that is interpreted as a null value (disabled by default).

        :param null_literal: The null literal string.
        :return: This :class:`Csv` object.
        """
        self._j_csv = self._j_csv.nullLiteral(null_literal)
        return self

    def schema(self, schema_data_type: DataType) -> 'Csv':
        """
        Sets the format schema with field names and the types. Required if schema is not derived.

        :param schema_data_type: Data type from :class:`DataTypes` that describes the schema.
        :return: This :class:`Csv` object.
        """
        self._j_csv = self._j_csv.schema(_to_java_type(schema_data_type))
        return self

    def derive_schema(self) -> 'Csv':
        """
        Derives the format schema from the table's schema. Required if no format schema is defined.

        This allows for defining schema information only once.

        The names, types, and fields' order of the format are determined by the table's
        schema. Time attributes are ignored if their origin is not a field. A "from" definition
        is interpreted as a field renaming in the format.

        :return: This :class:`Csv` object.
        """
        self._j_csv = self._j_csv.deriveSchema()
        return self


class Avro(FormatDescriptor):
    """
    Format descriptor for Apache Avro records.
    """

    def __init__(self, record_class=None, avro_schema=None):
        """
        Constructor of Avro format descriptor.

        :param record_class: The java fully-qualified class name of the Avro record.
        :param avro_schema: Avro schema string.
        """
        gateway = get_gateway()
        self._j_avro = gateway.jvm.Avro()
        super(Avro, self).__init__(self._j_avro)

        if record_class is not None:
            self.record_class(record_class)

        if avro_schema is not None:
            self.avro_schema(avro_schema)

    def record_class(self, record_class: str) -> 'Avro':
        """
        Sets the class of the Avro specific record.

        :param record_class: The java fully-qualified class name of the Avro record.
        :return: This object.
        """
        gateway = get_gateway()
        clz = gateway.jvm.Thread.currentThread().getContextClassLoader().loadClass(record_class)
        self._j_avro = self._j_avro.recordClass(clz)
        return self

    def avro_schema(self, avro_schema: str) -> 'Avro':
        """
        Sets the Avro schema for specific or generic Avro records.

        :param avro_schema: Avro schema string.
        :return: This object.
        """
        self._j_avro = self._j_avro.avroSchema(avro_schema)
        return self


class Json(FormatDescriptor):
    """
    Format descriptor for JSON.
    """

    def __init__(self, json_schema=None, schema=None, derive_schema=False):
        """
        Constructor of Json format descriptor.

        :param json_schema: The JSON schema string.
        :param schema: Sets the schema using :class:`DataTypes` that describes the schema.
        :param derive_schema: Derives the format schema from the table's schema described.
        """
        gateway = get_gateway()
        self._j_json = gateway.jvm.Json()
        super(Json, self).__init__(self._j_json)

        if json_schema is not None:
            self.json_schema(json_schema)

        if schema is not None:
            self.schema(schema)

        if derive_schema:
            self.derive_schema()

    def fail_on_missing_field(self, fail_on_missing_field: bool) -> 'Json':
        """
        Sets flag whether to fail if a field is missing or not.

        :param fail_on_missing_field: If set to ``True``, the operation fails if there is a missing
                                      field.
                                      If set to ``False``, a missing field is set to null.
        :return: This object.
        """
        if not isinstance(fail_on_missing_field, bool):
            raise TypeError("Only bool value is supported!")
        self._j_json = self._j_json.failOnMissingField(fail_on_missing_field)
        return self

    def ignore_parse_errors(self, ignore_parse_errors: bool) -> 'Json':
        """
        Sets flag whether to fail when parsing json fails.

        :param ignore_parse_errors: If set to true, the operation will ignore parse errors.
                                    If set to false, the operation fails when parsing json fails.
        :return: This object.
        """
        if not isinstance(ignore_parse_errors, bool):
            raise TypeError("Only bool value is supported!")
        self._j_json = self._j_json.ignoreParseErrors(ignore_parse_errors)
        return self

    def json_schema(self, json_schema: str) -> 'Json':
        """
        Sets the JSON schema string with field names and the types according to the JSON schema
        specification: http://json-schema.org/specification.html

        The schema might be nested.

        :param json_schema: The JSON schema string.
        :return: This object.
        """
        self._j_json = self._j_json.jsonSchema(json_schema)
        return self

    def schema(self, schema_data_type: DataType) -> 'Json':
        """
        Sets the schema using :class:`DataTypes`.

        JSON objects are represented as ROW types.

        The schema might be nested.

        :param schema_data_type: Data type that describes the schema.
        :return: This object.
        """
        self._j_json = self._j_json.schema(_to_java_type(schema_data_type))
        return self

    def derive_schema(self) -> 'Json':
        """
        Derives the format schema from the table's schema described.

        This allows for defining schema information only once.

        The names, types, and fields' order of the format are determined by the table's
        schema. Time attributes are ignored if their origin is not a field. A "from" definition
        is interpreted as a field renaming in the format.

        :return: This object.
        """
        self._j_json = self._j_json.deriveSchema()
        return self


class CustomFormatDescriptor(FormatDescriptor):
    """
    Describes the custom format of data.
    """

    def __init__(self, type, version):
        """
        Constructs a :class:`CustomFormatDescriptor`.

        :param type: String that identifies this format.
        :param version: Property version for backwards compatibility.
        """

        if not isinstance(type, str):
            raise TypeError("type must be of type str.")
        if not isinstance(version, int):
            raise TypeError("version must be of type int.")
        gateway = get_gateway()
        super(CustomFormatDescriptor, self).__init__(
            gateway.jvm.CustomFormatDescriptor(type, version))

    def property(self, key: str, value: str) -> 'CustomFormatDescriptor':
        """
        Adds a configuration property for the format.

        :param key: The property key to be set.
        :param value: The property value to be set.
        :return: This object.
        """

        if not isinstance(key, str):
            raise TypeError("key must be of type str.")
        if not isinstance(value, str):
            raise TypeError("value must be of type str.")
        self._j_format_descriptor = self._j_format_descriptor.property(key, value)
        return self

    def properties(self, property_dict: Dict[str, str]) -> 'CustomFormatDescriptor':
        """
        Adds a set of properties for the format.

        :param property_dict: The dict object contains configuration properties for the format.
                              Both the keys and values should be strings.
        :return: This object.
        """

        if not isinstance(property_dict, dict):
            raise TypeError("property_dict must be of type dict.")
        self._j_format_descriptor = self._j_format_descriptor.properties(property_dict)
        return self


class ConnectorDescriptor(Descriptor, metaclass=ABCMeta):
    """
    Describes a connector to an other system.
    """

    def __init__(self, j_connector_descriptor):
        self._j_connector_descriptor = j_connector_descriptor
        super(ConnectorDescriptor, self).__init__(self._j_connector_descriptor)


class FileSystem(ConnectorDescriptor):
    """
    Connector descriptor for a file system.
    """

    def __init__(self, path=None):
        """
        Constructor of FileSystem descriptor.

        :param path: The path of a file or directory.
        """
        gateway = get_gateway()
        self._j_file_system = gateway.jvm.FileSystem()
        super(FileSystem, self).__init__(self._j_file_system)

        if path is not None:
            self.path(path)

    def path(self, path_str: str) -> 'FileSystem':
        """
        Sets the path to a file or directory in a file system.

        :param path_str: The path of a file or directory.
        :return: This :class:`FileSystem` object.
        """
        self._j_file_system = self._j_file_system.path(path_str)
        return self


class Kafka(ConnectorDescriptor):
    """
    Connector descriptor for the Apache Kafka message queue.
    """

    def __init__(self, version=None, topic=None, properties=None, start_from_earliest=False,
                 start_from_latest=False, start_from_group_offsets=True,
                 start_from_specific_offsets_dict=None, start_from_timestamp=None,
                 sink_partitioner_fixed=None, sink_partitioner_round_robin=None,
                 custom_partitioner_class_name=None):
        """
        Constructor of Kafka descriptor.

        :param version: Kafka version. E.g., "0.8", "0.11", etc.
        :param topic: The topic from which the table is read.
        :param properties: The dict object contains configuration properties for the Kafka
                           consumer. Both the keys and values should be strings.
        :param start_from_earliest: Specifies the consumer to start reading from the earliest offset
                                    for all partitions.
        :param start_from_latest: Specifies the consumer to start reading from the latest offset for
                                  all partitions.
        :param start_from_group_offsets: Specifies the consumer to start reading from any committed
                                         group offsets found in Zookeeper / Kafka brokers.
        :param start_from_specific_offsets_dict: Dict of specific_offsets that the key is int-type
                                                 partition id and value is int-type offset value.
        :param start_from_timestamp: Specifies the consumer to start reading partitions from a
                                     specified timestamp.
        :param sink_partitioner_fixed: Configures how to partition records from Flink's partitions
                                       into Kafka's partitions.
        :param sink_partitioner_round_robin: Configures how to partition records from Flink's
                                             partitions into Kafka's partitions.
        :param custom_partitioner_class_name: Configures how to partition records from Flink's
                                              partitions into Kafka's partitions.
        """
        gateway = get_gateway()
        self._j_kafka = gateway.jvm.Kafka()
        super(Kafka, self).__init__(self._j_kafka)

        if version is not None:
            self.version(version)

        if topic is not None:
            self.topic(topic)

        if properties is not None:
            self.properties(properties)

        if start_from_earliest:
            self.start_from_earliest()

        if start_from_latest:
            self.start_from_latest()

        if start_from_group_offsets:
            self.start_from_group_offsets()

        if start_from_specific_offsets_dict is not None:
            self.start_from_specific_offsets(start_from_specific_offsets_dict)

        if start_from_timestamp is not None:
            self.start_from_timestamp(start_from_timestamp)

        if sink_partitioner_fixed is not None and sink_partitioner_fixed is True:
            self.sink_partitioner_fixed()

        if sink_partitioner_round_robin is not None and sink_partitioner_round_robin is True:
            self.sink_partitioner_round_robin()

        if custom_partitioner_class_name is not None:
            self.sink_partitioner_custom(custom_partitioner_class_name)

    def version(self, version: str) -> 'Kafka':
        """
        Sets the Kafka version to be used.

        :param version: Kafka version. E.g., "0.8", "0.11", etc.
        :return: This object.
        """
        if not isinstance(version, str):
            version = str(version)
        self._j_kafka = self._j_kafka.version(version)
        return self

    def topic(self, topic: str) -> 'Kafka':
        """
        Sets the topic from which the table is read.

        :param topic: The topic from which the table is read.
        :return: This object.
        """
        self._j_kafka = self._j_kafka.topic(topic)
        return self

    def properties(self, property_dict: Dict[str, str]) -> 'Kafka':
        """
        Sets the configuration properties for the Kafka consumer. Resets previously set properties.

        :param property_dict: The dict object contains configuration properties for the Kafka
                              consumer. Both the keys and values should be strings.
        :return: This object.
        """
        gateway = get_gateway()
        properties = gateway.jvm.java.util.Properties()
        for key in property_dict:
            properties.setProperty(key, property_dict[key])
        self._j_kafka = self._j_kafka.properties(properties)
        return self

    def property(self, key: str, value: str) -> 'Kafka':
        """
        Adds a configuration properties for the Kafka consumer.

        :param key: Property key string for the Kafka consumer.
        :param value: Property value string for the Kafka consumer.
        :return: This object.
        """
        self._j_kafka = self._j_kafka.property(key, value)
        return self

    def start_from_earliest(self) -> 'Kafka':
        """
        Specifies the consumer to start reading from the earliest offset for all partitions.
        This lets the consumer ignore any committed group offsets in Zookeeper / Kafka brokers.

        This method does not affect where partitions are read from when the consumer is restored
        from a checkpoint or savepoint. When the consumer is restored from a checkpoint or
        savepoint, only the offsets in the restored state will be used.

        :return: This object.
        """
        self._j_kafka = self._j_kafka.startFromEarliest()
        return self

    def start_from_latest(self) -> 'Kafka':
        """
        Specifies the consumer to start reading from the latest offset for all partitions.
        This lets the consumer ignore any committed group offsets in Zookeeper / Kafka brokers.

        This method does not affect where partitions are read from when the consumer is restored
        from a checkpoint or savepoint. When the consumer is restored from a checkpoint or
        savepoint, only the offsets in the restored state will be used.

        :return: This object.
        """
        self._j_kafka = self._j_kafka.startFromLatest()
        return self

    def start_from_group_offsets(self) -> 'Kafka':
        """
        Specifies the consumer to start reading from any committed group offsets found
        in Zookeeper / Kafka brokers. The "group.id" property must be set in the configuration
        properties. If no offset can be found for a partition, the behaviour in "auto.offset.reset"
        set in the configuration properties will be used for the partition.

        This method does not affect where partitions are read from when the consumer is restored
        from a checkpoint or savepoint. When the consumer is restored from a checkpoint or
        savepoint, only the offsets in the restored state will be used.

        :return: This object.
        """
        self._j_kafka = self._j_kafka.startFromGroupOffsets()
        return self

    def start_from_specific_offsets(self, specific_offsets_dict: Dict[int, int]) -> 'Kafka':
        """
        Specifies the consumer to start reading partitions from specific offsets, set independently
        for each partition. The specified offset should be the offset of the next record that will
        be read from partitions. This lets the consumer ignore any committed group offsets in
        Zookeeper / Kafka brokers.

        If the provided map of offsets contains entries whose partition is not subscribed by the
        consumer, the entry will be ignored. If the consumer subscribes to a partition that does
        not exist in the provided map of offsets, the consumer will fallback to the default group
        offset behaviour(see :func:`pyflink.table.descriptors.Kafka.start_from_group_offsets`)
        for that particular partition.

        If the specified offset for a partition is invalid, or the behaviour for that partition is
        defaulted to group offsets but still no group offset could be found for it, then the
        "auto.offset.reset" behaviour set in the configuration properties will be used for the
        partition.

        This method does not affect where partitions are read from when the consumer is restored
        from a checkpoint or savepoint. When the consumer is restored from a checkpoint or
        savepoint, only the offsets in the restored state will be used.

        :param specific_offsets_dict: Dict of specific_offsets that the key is int-type partition
                                      id and value is int-type offset value.
        :return: This object.
        """
        for key in specific_offsets_dict:
            self.start_from_specific_offset(key, specific_offsets_dict[key])
        return self

    def start_from_specific_offset(self, partition: int, specific_offset: int) -> 'Kafka':
        """
        Configures to start reading partitions from specific offsets and specifies the given offset
        for the given partition.

        see :func:`pyflink.table.descriptors.Kafka.start_from_specific_offsets`

        :param partition: Partition id.
        :param specific_offset: Specified offset in given partition.
        :return: This object.
        """
        self._j_kafka = self._j_kafka.startFromSpecificOffset(int(partition), int(specific_offset))
        return self

    def start_from_timestamp(self, timestamp: int) -> 'Kafka':
        """
        Specifies the consumer to start reading partitions from a specified timestamp.
        The specified timestamp must be before the current timestamp.
        This lets the consumer ignore any committed group offsets in Zookeeper / Kafka brokers.

        The consumer will look up the earliest offset whose timestamp is greater than or equal
        to the specific timestamp from Kafka. If there's no such offset, the consumer will use the
        latest offset to read data from kafka.

        This method does not affect where partitions are read from when the consumer is restored
        from a checkpoint or savepoint. When the consumer is restored from a checkpoint or
        savepoint, only the offsets in the restored state will be used.

        :param timestamp timestamp for the startup offsets, as milliseconds from epoch.
        :return: This object.

        .. versionadded:: 1.11.0
        """
        self._j_kafka = self._j_kafka.startFromTimestamp(int(timestamp))
        return self

    def sink_partitioner_fixed(self) -> 'Kafka':
        """
        Configures how to partition records from Flink's partitions into Kafka's partitions.

        This strategy ensures that each Flink partition ends up in one Kafka partition.

        .. note::

            One Kafka partition can contain multiple Flink partitions. Examples:

            More Flink partitions than Kafka partitions. Some (or all) Kafka partitions contain
            the output of more than one flink partition:

            |    Flink Sinks --------- Kafka Partitions
            |        1    ---------------->    1
            |        2    --------------/
            |        3    -------------/
            |        4    ------------/

            Fewer Flink partitions than Kafka partitions:

            |    Flink Sinks --------- Kafka Partitions
            |        1    ---------------->    1
            |        2    ---------------->    2
            |             ................     3
            |             ................     4
            |             ................     5

        :return: This object.
        """
        self._j_kafka = self._j_kafka.sinkPartitionerFixed()
        return self

    def sink_partitioner_round_robin(self) -> 'Kafka':
        """
        Configures how to partition records from Flink's partitions into Kafka's partitions.

        This strategy ensures that records will be distributed to Kafka partitions in a
        round-robin fashion.

        .. note::

            This strategy is useful to avoid an unbalanced partitioning. However, it will cause a
            lot of network connections between all the Flink instances and all the Kafka brokers.

        :return: This object.
        """
        self._j_kafka = self._j_kafka.sinkPartitionerRoundRobin()
        return self

    def sink_partitioner_custom(self, partitioner_class_name: str) -> 'Kafka':
        """
        Configures how to partition records from Flink's partitions into Kafka's partitions.

        This strategy allows for a custom partitioner by providing an implementation
        of ``FlinkKafkaPartitioner``.

        :param partitioner_class_name: The java canonical class name of the FlinkKafkaPartitioner.
                                       The FlinkKafkaPartitioner must have a public no-argument
                                       constructor and can be founded by in current Java
                                       classloader.
        :return: This object.
        """
        gateway = get_gateway()
        self._j_kafka = self._j_kafka.sinkPartitionerCustom(
            gateway.jvm.Thread.currentThread().getContextClassLoader()
                   .loadClass(partitioner_class_name))
        return self


class Elasticsearch(ConnectorDescriptor):
    """
    Connector descriptor for the Elasticsearch search engine.
    """

    def __init__(self, version=None, hostname=None, port=None, protocol=None, index=None,
                 document_type=None, key_delimiter=None, key_null_literal=None,
                 failure_handler_fail=False, failure_handler_ignore=False,
                 failure_handler_retry_rejected=False, custom_failure_handler_class_name=None,
                 disable_flush_on_checkpoint=False, bulk_flush_max_actions=None,
                 bulk_flush_max_size=None, bulk_flush_interval=None,
                 bulk_flush_backoff_constant=False, bulk_flush_backoff_exponential=False,
                 bulk_flush_backoff_max_retries=None, bulk_flush_backoff_delay=None,
                 connection_max_retry_timeout=None, connection_path_prefix=None):
        """
        Constructor of Elasticsearch descriptor.

        :param version: Elasticsearch version. E.g., "6".
        :param hostname: Connection hostname.
        :param port: Connection port.
        :param protocol: Connection protocol; e.g. "http".
        :param index: Elasticsearch index.
        :param document_type: Elasticsearch document type.
        :param key_delimiter: Key delimiter; e.g., "$" would result in IDs "KEY1$KEY2$KEY3".
        :param key_null_literal: key null literal string; e.g. "N/A" would result in IDs
                                 "KEY1_N/A_KEY3".
        :param failure_handler_fail: Configures a failure handling strategy in case a request to
                                     Elasticsearch fails.
        :param failure_handler_ignore: Configures a failure handling strategy in case a request to
                                       Elasticsearch fails.
        :param failure_handler_retry_rejected: Configures a failure handling strategy in case a
                                               request to Elasticsearch fails.
        :param custom_failure_handler_class_name: Configures a failure handling strategy in case a
                                                  request to Elasticsearch fails.
        :param disable_flush_on_checkpoint: Disables flushing on checkpoint
        :param bulk_flush_max_actions: the maximum number of actions to buffer per bulk request.
        :param bulk_flush_max_size: The maximum size. E.g. "42 mb". only MB granularity is
                                    supported.
        :param bulk_flush_interval: Bulk flush interval (in milliseconds).
        :param bulk_flush_backoff_constant: Configures how to buffer elements before sending them in
                                            bulk to the cluster for efficiency.
        :param bulk_flush_backoff_exponential: Configures how to buffer elements before sending them
                                               in bulk to the cluster for efficiency.
        :param bulk_flush_backoff_max_retries: The maximum number of retries.
        :param bulk_flush_backoff_delay: Delay between each backoff attempt (in milliseconds).
        :param connection_max_retry_timeout: Maximum timeout (in milliseconds).
        :param connection_path_prefix: Prefix string to be added to every REST communication.
        """
        gateway = get_gateway()
        self._j_elasticsearch = gateway.jvm.Elasticsearch()
        super(Elasticsearch, self).__init__(self._j_elasticsearch)

        if version is not None:
            self.version(version)

        if hostname is not None:
            self.host(hostname, port, protocol)

        if index is not None:
            self.index(index)

        if document_type is not None:
            self.document_type(document_type)

        if key_delimiter is not None:
            self.key_delimiter(key_delimiter)

        if key_null_literal is not None:
            self.key_null_literal(key_null_literal)

        if failure_handler_fail:
            self.failure_handler_fail()

        if failure_handler_ignore:
            self.failure_handler_ignore()

        if failure_handler_retry_rejected:
            self.failure_handler_retry_rejected()

        if custom_failure_handler_class_name is not None:
            self.failure_handler_custom(custom_failure_handler_class_name)

        if disable_flush_on_checkpoint:
            self.disable_flush_on_checkpoint()

        if bulk_flush_max_actions is not None:
            self.bulk_flush_max_actions(bulk_flush_max_actions)

        if bulk_flush_max_size is not None:
            self.bulk_flush_max_size(bulk_flush_max_size)

        if bulk_flush_interval is not None:
            self.bulk_flush_interval(bulk_flush_interval)

        if bulk_flush_backoff_constant:
            self.bulk_flush_backoff_constant()

        if bulk_flush_backoff_exponential:
            self.bulk_flush_backoff_exponential()

        if bulk_flush_backoff_max_retries is not None:
            self.bulk_flush_backoff_max_retries(bulk_flush_backoff_max_retries)

        if bulk_flush_backoff_delay is not None:
            self.bulk_flush_backoff_delay(bulk_flush_backoff_delay)

        if connection_max_retry_timeout is not None:
            self.connection_max_retry_timeout(connection_max_retry_timeout)

        if connection_path_prefix is not None:
            self.connection_path_prefix(connection_path_prefix)

    def version(self, version: str) -> 'Elasticsearch':
        """
        Sets the Elasticsearch version to be used. Required.

        :param version: Elasticsearch version. E.g., "6".
        :return: This object.
        """
        if not isinstance(version, str):
            version = str(version)
        self._j_elasticsearch = self._j_elasticsearch.version(version)
        return self

    def host(self, hostname: str, port: Union[int, str], protocol: str) -> 'Elasticsearch':
        """
        Adds an Elasticsearch host to connect to. Required.

        Multiple hosts can be declared by calling this method multiple times.

        :param hostname: Connection hostname.
        :param port: Connection port.
        :param protocol: Connection protocol; e.g. "http".
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.host(hostname, int(port), protocol)
        return self

    def index(self, index: str) -> 'Elasticsearch':
        """
        Declares the Elasticsearch index for every record. Required.

        :param index: Elasticsearch index.
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.index(index)
        return self

    def document_type(self, document_type: str) -> 'Elasticsearch':
        """
        Declares the Elasticsearch document type for every record. Required.

        :param document_type: Elasticsearch document type.
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.documentType(document_type)
        return self

    def key_delimiter(self, key_delimiter: str) -> 'Elasticsearch':
        """
        Sets a custom key delimiter in case the Elasticsearch ID needs to be constructed from
        multiple fields. Optional.

        :param key_delimiter: Key delimiter; e.g., "$" would result in IDs "KEY1$KEY2$KEY3".
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.keyDelimiter(key_delimiter)
        return self

    def key_null_literal(self, key_null_literal: str) -> 'Elasticsearch':
        """
        Sets a custom representation for null fields in keys. Optional.

        :param key_null_literal: key null literal string; e.g. "N/A" would result in IDs
                                 "KEY1_N/A_KEY3".
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.keyNullLiteral(key_null_literal)
        return self

    def failure_handler_fail(self) -> 'Elasticsearch':
        """
        Configures a failure handling strategy in case a request to Elasticsearch fails.

        This strategy throws an exception if a request fails and thus causes a job failure.

        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.failureHandlerFail()
        return self

    def failure_handler_ignore(self) -> 'Elasticsearch':
        """
        Configures a failure handling strategy in case a request to Elasticsearch fails.

        This strategy ignores failures and drops the request.

        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.failureHandlerIgnore()
        return self

    def failure_handler_retry_rejected(self) -> 'Elasticsearch':
        """
        Configures a failure handling strategy in case a request to Elasticsearch fails.

        This strategy re-adds requests that have failed due to queue capacity saturation.

        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.failureHandlerRetryRejected()
        return self

    def failure_handler_custom(self, failure_handler_class_name: str) -> 'Elasticsearch':
        """
        Configures a failure handling strategy in case a request to Elasticsearch fails.

        This strategy allows for custom failure handling using a ``ActionRequestFailureHandler``.

        :param failure_handler_class_name:
        :return: This object.
        """
        gateway = get_gateway()
        self._j_elasticsearch = self._j_elasticsearch.failureHandlerCustom(
            gateway.jvm.Thread.currentThread().getContextClassLoader()
                   .loadClass(failure_handler_class_name))
        return self

    def disable_flush_on_checkpoint(self) -> 'Elasticsearch':
        """
        Disables flushing on checkpoint. When disabled, a sink will not wait for all pending action
        requests to be acknowledged by Elasticsearch on checkpoints.

        .. note::

            If flushing on checkpoint is disabled, a Elasticsearch sink does NOT
            provide any strong guarantees for at-least-once delivery of action requests.

        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.disableFlushOnCheckpoint()
        return self

    def bulk_flush_max_actions(self, max_actions_num: int) -> 'Elasticsearch':
        """
        Configures how to buffer elements before sending them in bulk to the cluster for
        efficiency.

        Sets the maximum number of actions to buffer for each bulk request.

        :param max_actions_num: the maximum number of actions to buffer per bulk request.
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.bulkFlushMaxActions(int(max_actions_num))
        return self

    def bulk_flush_max_size(self, max_size: int) -> 'Elasticsearch':
        """
        Configures how to buffer elements before sending them in bulk to the cluster for
        efficiency.

        Sets the maximum size of buffered actions per bulk request (using the syntax of
        MemorySize).

        :param max_size: The maximum size. E.g. "42 mb". only MB granularity is supported.
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.bulkFlushMaxSize(max_size)
        return self

    def bulk_flush_interval(self, interval: int) -> 'Elasticsearch':
        """
        Configures how to buffer elements before sending them in bulk to the cluster for
        efficiency.

        Sets the bulk flush interval (in milliseconds).

        :param interval: Bulk flush interval (in milliseconds).
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.bulkFlushInterval(int(interval))
        return self

    def bulk_flush_backoff_constant(self) -> 'Elasticsearch':
        """
        Configures how to buffer elements before sending them in bulk to the cluster for
        efficiency.

        Sets a constant backoff type to use when flushing bulk requests.

        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.bulkFlushBackoffConstant()
        return self

    def bulk_flush_backoff_exponential(self) -> 'Elasticsearch':
        """
        Configures how to buffer elements before sending them in bulk to the cluster for
        efficiency.

        Sets an exponential backoff type to use when flushing bulk requests.

        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.bulkFlushBackoffExponential()
        return self

    def bulk_flush_backoff_max_retries(self, max_retries: int) -> 'Elasticsearch':
        """
        Configures how to buffer elements before sending them in bulk to the cluster for
        efficiency.

        Sets the maximum number of retries for a backoff attempt when flushing bulk requests.

        Make sure to enable backoff by selecting a strategy (
        :func:`pyflink.table.descriptors.Elasticsearch.bulk_flush_backoff_constant` or
        :func:`pyflink.table.descriptors.Elasticsearch.bulk_flush_backoff_exponential`).

        :param max_retries: The maximum number of retries.
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.bulkFlushBackoffMaxRetries(int(max_retries))
        return self

    def bulk_flush_backoff_delay(self, delay: int) -> 'Elasticsearch':
        """
        Configures how to buffer elements before sending them in bulk to the cluster for
        efficiency.

        Sets the amount of delay between each backoff attempt when flushing bulk requests
        (in milliseconds).

        Make sure to enable backoff by selecting a strategy (
        :func:`pyflink.table.descriptors.Elasticsearch.bulk_flush_backoff_constant` or
        :func:`pyflink.table.descriptors.Elasticsearch.bulk_flush_backoff_exponential`).

        :param delay: Delay between each backoff attempt (in milliseconds).
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.bulkFlushBackoffDelay(int(delay))
        return self

    def connection_max_retry_timeout(self, max_retry_timeout: int) -> 'Elasticsearch':
        """
        Sets connection properties to be used during REST communication to Elasticsearch.

        Sets the maximum timeout (in milliseconds) in case of multiple retries of the same request.

        :param max_retry_timeout: Maximum timeout (in milliseconds).
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.connectionMaxRetryTimeout(
            int(max_retry_timeout))
        return self

    def connection_path_prefix(self, path_prefix: str) -> 'Elasticsearch':
        """
        Sets connection properties to be used during REST communication to Elasticsearch.

        Adds a path prefix to every REST communication.

        :param path_prefix: Prefix string to be added to every REST communication.
        :return: This object.
        """
        self._j_elasticsearch = self._j_elasticsearch.connectionPathPrefix(path_prefix)
        return self


class CustomConnectorDescriptor(ConnectorDescriptor):
    """
    Describes a custom connector to an other system.
    """

    def __init__(self, type, version, format_needed):
        """
        Constructs a :class:`CustomConnectorDescriptor`.

        :param type: String that identifies this connector.
        :param version: Property version for backwards compatibility.
        :param format_needed: Flag for basic validation of a needed format descriptor.
        """

        if not isinstance(type, str):
            raise TypeError("type must be of type str.")
        if not isinstance(version, int):
            raise TypeError("version must be of type int.")
        if not isinstance(format_needed, bool):
            raise TypeError("format_needed must be of type bool.")
        gateway = get_gateway()
        super(CustomConnectorDescriptor, self).__init__(
            gateway.jvm.CustomConnectorDescriptor(type, version, format_needed))

    def property(self, key: str, value: str) -> 'CustomConnectorDescriptor':
        """
        Adds a configuration property for the connector.

        :param key: The property key to be set.
        :param value: The property value to be set.
        :return: This object.
        """

        if not isinstance(key, str):
            raise TypeError("key must be of type str.")
        if not isinstance(value, str):
            raise TypeError("value must be of type str.")
        self._j_connector_descriptor = self._j_connector_descriptor.property(key, value)
        return self

    def properties(self, property_dict: Dict[str, str]) -> 'CustomConnectorDescriptor':
        """
        Adds a set of properties for the connector.

        :param property_dict: The dict object contains configuration properties for the connector.
                              Both the keys and values should be strings.
        :return: This object.
        """

        if not isinstance(property_dict, dict):
            raise TypeError("property_dict must be of type dict.")
        self._j_connector_descriptor = self._j_connector_descriptor.properties(property_dict)
        return self


class ConnectTableDescriptor(Descriptor, metaclass=ABCMeta):
    """
    Common class for table's created with :class:`pyflink.table.TableEnvironment.connect`.
    """

    def __init__(self, j_connect_table_descriptor):
        self._j_connect_table_descriptor = j_connect_table_descriptor
        super(ConnectTableDescriptor, self).__init__(self._j_connect_table_descriptor)

    def with_format(self, format_descriptor: FormatDescriptor) -> 'ConnectTableDescriptor':
        """
        Specifies the format that defines how to read data from a connector.

        :type format_descriptor: The :class:`FormatDescriptor` for the resulting table,
                                 e.g. :class:`OldCsv`.
        :return: This object.
        """
        self._j_connect_table_descriptor = \
            self._j_connect_table_descriptor.withFormat(format_descriptor._j_format_descriptor)
        return self

    def with_schema(self, schema: Schema) -> 'ConnectTableDescriptor':
        """
        Specifies the resulting table schema.

        :type schema: The :class:`Schema` object for the resulting table.
        :return: This object.
        """
        self._j_connect_table_descriptor = \
            self._j_connect_table_descriptor.withSchema(schema._j_schema)
        return self

    def create_temporary_table(self, path: str) -> 'ConnectTableDescriptor':
        """
        Registers the table described by underlying properties in a given path.

        There is no distinction between source and sink at the descriptor level anymore as this
        method does not perform actual class lookup. It only stores the underlying properties. The
        actual source/sink lookup is performed when the table is used.

        Temporary objects can shadow permanent ones. If a permanent object in a given path exists,
        it will be inaccessible in the current session. To make the permanent object available
        again you can drop the corresponding temporary object.

        .. note:: The schema must be explicitly defined.

        :param path: path where to register the temporary table

        .. versionadded:: 1.10.0
        """
        self._j_connect_table_descriptor.createTemporaryTable(path)
        return self


class StreamTableDescriptor(ConnectTableDescriptor):
    """
    Descriptor for specifying a table source and/or sink in a streaming environment.

    .. seealso:: parent class: :class:`ConnectTableDescriptor`
    """

    def __init__(self, j_stream_table_descriptor, in_append_mode=False, in_retract_mode=False,
                 in_upsert_mode=False):
        """
        Constructor of StreamTableDescriptor.

        :param j_stream_table_descriptor: the corresponding stream table descriptor in java.
        :param in_append_mode: Declares the conversion between a dynamic table and external
                               connector in append mode.
        :param in_retract_mode: Declares the conversion between a dynamic table and external
                               connector in retract mode.
        :param in_upsert_mode: Declares the conversion between a dynamic table and external
                               connector in upsert mode.
        """
        self._j_stream_table_descriptor = j_stream_table_descriptor
        super(StreamTableDescriptor, self).__init__(self._j_stream_table_descriptor)

        if in_append_mode:
            self.in_append_mode()

        if in_retract_mode:
            self.in_retract_mode()

        if in_upsert_mode:
            self.in_upsert_mode()

    def in_append_mode(self) -> 'StreamTableDescriptor':
        """
        Declares how to perform the conversion between a dynamic table and an external connector.

        In append mode, a dynamic table and an external connector only exchange INSERT messages.

        :return: This object.
        """
        self._j_stream_table_descriptor = self._j_stream_table_descriptor.inAppendMode()
        return self

    def in_retract_mode(self) -> 'StreamTableDescriptor':
        """
        Declares how to perform the conversion between a dynamic table and an external connector.

        In retract mode, a dynamic table and an external connector exchange ADD and RETRACT
        messages.

        An INSERT change is encoded as an ADD message, a DELETE change as a RETRACT message, and an
        UPDATE change as a RETRACT message for the updated (previous) row and an ADD message for
        the updating (new) row.

        In this mode, a key must not be defined as opposed to upsert mode. However, every update
        consists of two messages which is less efficient.

        :return: This object.
        """
        self._j_stream_table_descriptor = self._j_stream_table_descriptor.inRetractMode()
        return self

    def in_upsert_mode(self) -> 'StreamTableDescriptor':
        """
        Declares how to perform the conversion between a dynamic table and an external connector.

        In upsert mode, a dynamic table and an external connector exchange UPSERT and DELETE
        messages.

        This mode requires a (possibly composite) unique key by which updates can be propagated. The
        external connector needs to be aware of the unique key attribute in order to apply messages
        correctly. INSERT and UPDATE changes are encoded as UPSERT messages. DELETE changes as
        DELETE messages.

        The main difference to a retract stream is that UPDATE changes are encoded with a single
        message and are therefore more efficient.

        :return: This object.
        """
        self._j_stream_table_descriptor = self._j_stream_table_descriptor.inUpsertMode()
        return self
