class BaseAdapter():
    """
    Interface class for a prov database adapter
    """

    def __init__(self, *args, **kwargs):
        pass

    def connect(self, authentication_info):
        """
        Establish the database connection / login into the database

        :param authentication_info: a custom dict with credentials
        :type authentication_info: dict
        :return: Indicate whether the connection was successful
        :rtype: boolean
        :raise InvalidOptionsException:
        """
        raise NotImplementedError("Abstract method")

    def save_element(self, attributes, metadata):
        """
        Saves a entity, activity or entity into the database

        :param attributes: Attributes as dict for the record. Be careful you have to encode the dict
        :type attributes: dict
        :param metadata: Metadata as dict for the record. Be careful you have to encode the dict but you can be sure that all meta keys are always there
        :type metadata: dict
        :return: Record id
        :rtype: str
        """
        raise NotImplementedError("Abstract method")

    def save_relation(self, from_node, to_node, attributes, metadata):
        """
        Create a relation between 2 nodes

        :param from_node: The identifier
        :type from_node: str
        :param to_node: The identifier for the destination node
        :type: to_node: str
        :param attributes:  Attributes as dict for the record. Be careful you have to encode the dict
        :type attributes: dict
        :param metadata: Metadata as dict for the record. Be careful you have to encode the dict but you can be sure that all meta keys are always there
        :type metadata: dict
        :return: Record id
        :rtype: str
        """
        raise NotImplementedError("Abstract method")

    def get_records_by_filter(self, attributes_dict=None, metadata_dict=None):
        """
        Returns all records (nodes and relations) based on a filter dict.
        The filter dict's are and AND combination but only the start node must fulfill the conditions.
        The result should contain all associated relations and nodes together

        :param attributes_dict:
        :type attributes_dict: dict
        :param metadata_dict:
        :type metadata_dict: dict
        :return: list of relations and nodes
        :rtype: list
        """
        raise NotImplementedError("Abstract method")

    def get_records_tail(self, attributes_dict=None, metadata_dict=None, depth=None):
        """
        Returns all connected nodes and relations based on a filter.
        The filter is an AND combination and this describes the filter only for the origin nodes.

        :param attributes_dict:
        :type attributes_dict: dict
        :param metadata_dict:
        :type metadata_dict: dict
        :param depth:
        :type depth: int
        :return: a list of relations and nodes
        :rtype: list
        """
        raise NotImplementedError("Abstract method")

    def get_bundle_records(self, bundle_identifier):
        """
        Returns the relations and nodes for a specific bundle identifier.
        Please use the bundle association to get all bundle nodes.
        Only the relations belongs to the bundle where the start AND end node belong also to the bundle.
        Except the prov:Mention see: W3C bundle links

        :param bundle_identifier: The bundle identifier
        :type bundle_identifier: str
        :return: list of nodes and bundles
        :rtype: list
        """
        raise NotImplementedError("Abstract method")

    def get_record(self, record_id):
        """
        Return a single record

        :param record_id: The id
        :type record_id: str
        :return: DbRecord
        :rtype: DbRecord
        """
        raise NotImplementedError("Abstract method")

    def get_relation(self, relation_id):
        """
        Returns a single relation

        :param relation_id: The id
        :type relation_id: str
        :return: DbRelation
        :rtype: DbRelation
        """
        raise NotImplementedError("Abstract method")

    def delete_records_by_filter(self, attributes_dict, metadata_dict):
        """
        Delete records by filter

        :param attributes_dict:
        :type attributes_dict: dict
        :param metadata_dict:
        :type metadata_dict: dict
        :return: Indicates whether the deletion was successful
        :rtype: boolean
        :raise NotFoundException:
        """
        raise NotImplementedError("Abstract method")

    def delete_record(self, record_id):
        """
        Delete a single record

        :param record_id:
        :type record_id: str
        :return: Indicates whether the deletion was successful
        :rtype: boolean
        :raise NotFoundException:
        """
        raise NotImplementedError("Abstract method")

    def delete_relation(self, relation_id):
        """
        Delete a single relation

        :param relation_id:
        :type relation_id: str
        :return: Indicates whether the deletion was successful
        :rtype: boolean
        :raise NotFoundException:
        """
        raise NotImplementedError("Abstract method") METADATA_KEY_PROV_TYPE = "prov_type" METADATA_KEY_TYPE_MAP = "type_map" METADATA_KEY_IDENTIFIER = "identifier" METADATA_KEY_NAMESPACES = "namespaces" class InvalidOptionsException(AdapterException):
    """
    Thrown, if passed argument for adapter is invalid.
    """
    pass class AuthException(AdapterException):
    """
    Thrown, if database adapter could not establish a connection with given credentials to the database.
    """
    pass class DatabaseException(AdapterException):
    """
    Thrown, if method could not performed on database.
    """
    pass class CreateRecordException(DatabaseException):
    """
    Thrown, if record could not be saved in database.
    """
    pass class NotFoundException(DatabaseException):
    """
    Thrown, if record or relation could not be found in database.
    """
    pass class CreateRelationException(DatabaseException):
    """
    Thrown, if relation could not be saved in database.
    """
    pass class MergeException(DatabaseException):
    """
    Thrown, if a record or relation can't get merged
    """
    pass def encode_string_value_to_primitive(value):
    """
    Convert a value into one of the following types:

    - dict
    - str
    - float
    - int
    - list


    :param value:
    :return:
    """
    if isinstance(value, Literal):
        return value.value
    elif type(value) is int:
        return value
    elif type(value) is float:
        return value
    elif type(value) is bool:
        return value
    elif type(value) is list:
        return value
    elif type(value) is dict:
        io = StringIO()
        json.dump(value, io)
        return io.getvalue()
    return str(value) def encode_dict_values_to_primitive(dict_values):
    """
    This function transforms a dict with all kind of types into a dict with only

    - str
    - dict
    - book
    - str

    values

    :param dict_values:
    :return:
    """
    new_dict_values = dict()
    for key, value in dict_values.items():
        key_simple = str(key)
        new_dict_values.update({key_simple: encode_string_value_to_primitive(value)})

    return new_dict_values def split_into_formal_and_other_attributes(attributes, metadata):
    """
    This function split the attributes and metadata into formal attributes and other attributes.
    Helpful for merge operations and searching for duplicate relations

    :param attributes:
    :param metadata:
    :return: namedtuple(formal_attributes, other_attributes)
    :rtype: FormalAndOtherAttributes
    """
    prov_type = metadata[METADATA_KEY_PROV_TYPE]

    if str(prov_type) == "prov:Unknown":
        formal_qualified_names = set()
    else:
        class_type = PROV_REC_CLS[prov_type]
        formal_qualified_names = class_type.FORMAL_ATTRIBUTES

    formal_attributes = {key: attributes[key] for key in attributes.keys() if key in formal_qualified_names}
    other_attributes = {key: attributes[key] for key in attributes.keys() if key not in formal_qualified_names}

    return FormalAndOtherAttributes(formal_attributes, other_attributes)
import os
import provdbconnector.db_adapters.neo4j.cypher_commands as cypher_commands
import logging
from neo4j.exceptions import ConfigurationError
from neo4j.graph import Relationship
from provdbconnector.db_adapters.baseadapter import BaseAdapter
from provdbconnector.db_adapters.baseadapter import METADATA_KEY_PROV_TYPE, METADATA_KEY_TYPE_MAP, \
    METADATA_KEY_IDENTIFIER, METADATA_KEY_NAMESPACES
from provdbconnector.exceptions.database import InvalidOptionsException, AuthException, \
    DatabaseException, CreateRecordException, NotFoundException, CreateRelationException, MergeException
from neo4j import GraphDatabase, basic_auth
from prov.constants import PROV_N_MAP
from collections import namedtuple
from provdbconnector.utils.serializer import encode_string_value_to_primitive, encode_dict_values_to_primitive, \
    split_into_formal_and_other_attributes
        prefixed_metadata = dict()
        for key, value in metadata.items():
            prefixed_metadata["{meta_prefix}{key}".format(key=key, meta_prefix=NEO4J_META_PREFIX)] = value

        return prefixed_metadata

    @staticmethod
    def _parse_to_primitive_attributes(attributes, prefixed_metadata):
        """
        Convert the dict values and keys into a neo4j friendly type (dict=>json, list,int,float, QualifiedName=>str, datetime=>str)

        :param attributes:
        :param prefixed_metadata:
        :return:
        """
        all_attributes = attributes.copy()
        all_attributes.update(prefixed_metadata)

        db_attributes = dict()
        # transform values
        for key, value in all_attributes.items():
            key_primitive = encode_string_value_to_primitive(key)
            value_primitive = encode_string_value_to_primitive(value)
            db_attributes.update({key_primitive: value_primitive})

        return db_attributes

    @staticmethod
    def _get_attributes_identifiers_cypher_string(key_list):
        """
        This function return a cypher string with all keys as cypher parameters

        :param key_list:
        :return:
        """
        db_attributes_identifiers = map(lambda key: "`{}`: {{`{}`}}".format(key, key), key_list)
        return ",".join(db_attributes_identifiers)

    @staticmethod
    def _get_attributes_set_cypher_string(key_list, cypher_template=cypher_commands.NEO4J_CREATE_NODE_SET_PART):
        """
        Returns a set cypher command for all keys of the keylist

        :param key_list:
        :param cypher_template:
        :return:
        """
        statements = list()
        for key in key_list:
            statements.append(cypher_template.format(attr_name=key))
        return " ".join(statements)

    def save_element(self, attributes, metadata):
        """
        Saves a single record

        :param attributes: The attributes dict
        :type attributes: dict
        :param metadata: The metadata dict
        :type metadata: dict
        :return: The id of the record
        :rtype: str
        """

        metadata = metadata.copy()
        prefixed_metadata = self._prefix_metadata(metadata)

        # setup merge attributes
        (formal_attributes, other_attributes) = split_into_formal_and_other_attributes(attributes, metadata)

        merge_relevant_keys = list()
        merge_relevant_keys.append("meta:{}".format(METADATA_KEY_IDENTIFIER))
        merge_relevant_keys = merge_relevant_keys + list(formal_attributes.keys())

        other_db_attribute_keys = list()
        other_db_attribute_keys = other_db_attribute_keys + list(other_attributes.keys())
        other_db_attribute_keys = other_db_attribute_keys + list(prefixed_metadata.keys())

        # get set statement for non formal attributes
        attr_for_simple_set = other_db_attribute_keys.copy()
        attr_for_simple_set.remove("meta:" + METADATA_KEY_NAMESPACES)
        attr_for_simple_set.remove("meta:" + METADATA_KEY_TYPE_MAP)
        cypher_set_statement = self._get_attributes_set_cypher_string(attr_for_simple_set)

        attr_for_list_merge = list()
        attr_for_list_merge.append("meta:" + METADATA_KEY_NAMESPACES)
        attr_for_list_merge.append("meta:" + METADATA_KEY_TYPE_MAP)
        cypher_set_statement += self._get_attributes_set_cypher_string(attr_for_list_merge,
                                                                       cypher_commands.NEO4J_CREATE_NODE_SET_PART_MERGE_ATTR)

        # get CASE WHEN ... statement to check if a attribute is different
        cypher_merge_check_statement = self._get_attributes_set_cypher_string(attr_for_simple_set,
                                                                              cypher_commands.NEO4J_CREATE_NODE_MERGE_CHECK_PART)

        # get cypher string for the merge relevant attributes
        cypher_merge_relevant_str = self._get_attributes_identifiers_cypher_string(merge_relevant_keys)

        # get prov type
        provtype = metadata[METADATA_KEY_PROV_TYPE]

        # get db_attributes as dict
        db_attributes = self._parse_to_primitive_attributes(attributes, prefixed_metadata)

        session = self._create_session()

        command = cypher_commands.NEO4J_CREATE_NODE_RETURN_ID.format(label=provtype.localpart,
                                                     formal_attributes=cypher_merge_relevant_str,
                                                     set_statement=cypher_set_statement,
                                                     merge_check_statement=cypher_merge_check_statement)
        with session.begin_transaction() as tx:

            result = tx.run(command, dict(db_attributes))

            record_id = None
            merge_success = 0
            for record in result:
                record_id = record["ID"]
                merge_success = record["check"]

            if record_id is None:

                raise CreateRecordException("No ID property returned by database for the command {}".format(command))