#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable-msg=C0103
################################################################################
# Copyright (c) 2006-2017 Franz Inc.  
# All rights reserved. This program and the accompanying materials are
# made available under the terms of the MIT License which accompanies
# this distribution, and is available at http://opensource.org/licenses/MIT
################################################################################
from __future__ import absolute_import
from __future__ import unicode_literals
from builtins import str
from builtins import map
from builtins import object
from future import standard_library
standard_library.install_aliases()
import os
from ..exceptions import ServerException
from ..repository.repository import Repository, RepositoryConnection
from ...miniclient import repository as miniserver
import re, urllib.request, urllib.parse, urllib.error
from . import spec
from past.builtins import basestring
READ_ONLY = 'READ_ONLY'
LEGAL_OPTION_TYPES = {READ_ONLY: bool,}
[docs]class AllegroGraphServer(object):
    """
    The AllegroGraphServer object represents a remote AllegroGraph server on
    the network. It is used to inventory and access the catalogs of that
    server.
    """
[docs]    def __init__(self, host=None, port=None, user=None, password=None,
                 cainfo=None, sslcert=None, verifyhost=None, verifypeer=None,
                 protocol=None,
                 proxy=os.environ.get('AGRAPH_PROXY'),
                 **options):
        """
        Define the connection to the AllegroGraph HTTP server.
        Pass either ``user`` and ``password`` for Basic Authentication or
        ``cainfo``, ``sslcert`` values for client X.509 certificate
        authentication.
        :param host: Address of the AllegroGraph server to connect to.
                     This can also include protocol and port
                     (e.g. http://localhost:10035). Can also be specified in
                     the ``AGRAPH_HOST`` environment variable.
                     The default value is ``'localhost'```.
        :type host: string
        :param port: Port on which the server is listening.
                     The default is 10035 if ``protocol`` is ``'http'``
                     and 10036 if it is ``'https'``. Can also be specified in
                     the ``AGRAPH_PORT`` environment variable.
                     If passed explicitly this parameter overrides any value
                     that might have been specified as a part of the ``host`` string.
        :type port: int
        :param protocol: Connection protocol, either ``'http'`` or ``'https'``.
                         The default is ``'http'`` if no SSL parameters are set
                         and `'https'`` otherwise.
                         If passed explicitly this parameter overrides any value
                         that might have been specified as a part of the ``host`` string.
        :type protocol: string
        :param user: Username (when using Basic authentication). Can also be specified in
                     the ``AGRAPH_USER`` environment variable.
        :type user: string
        :param password: Password (when using Basic authentication). Can also be specified in
                         the ``AGRAPH_PASSWORD`` environment variable.
        :type password: string
        :param cainfo: Path to a file or directory containing CA certificates that
                       will be used to validate the server's certificate.
        :type cainfo: string
        :param sslcert: Client certificate path (when using SSL authentication).
        :type sslcert: string
        :param verifyhost: If set to ``0`` it will not be an error if the server's
                           SSL certificate does not match the server's address.
                           The default value is ``2``, meaning that the host name will
                           be validated against the certificate.
                           ..seealso:: https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYHOST.html
        :type verifyhost: int
        :param verifypeer: If set to ``1`` (the default) the validity of the server's
                           SSL certificate will be checked. Set to ``0`` to disable
                           the validation.
                           ..seealso:: https://curl.haxx.se/libcurl/c/CURLOPT_SSL_VERIFYPEER.html
        :type verifypeer: int
        :param proxy: Proxy specification string. The format is SCHEME://HOST:PORT.
                      Supported schemes are 'http', 'socks4' and 'socks5'.
                      Note that for SOCKS proxies DNS requests are performed by the
                      proxy server.
                      The default value is taken from the AGRAPH_PROXY environment
                      variable.
        :type proxy: string
        :param options: Ignored.
        """
        # Not sure why we accept these, but don't want to change the API at this point.
        del options
        host = host or os.environ.get('AGRAPH_HOST', '127.0.0.1')
        # Check if other arguments were passed as a part of host
        match = re.match(r'^(?:(?P<protocol>https?)://)?'
                         r'(?P<host>[^:]*)(?::(?P<port>[0-9]*))?(?P<tail>.*)$',
                         host)
        if match:
            if protocol is None:
                protocol = match.group('protocol')
            if port is None and match.group('port') is not None:
                port = int(match.group('port'))
            host = match.group('host')
            tail = match.group('tail')
        else:
            tail = ''
        has_https_params = cainfo or sslcert or verifyhost is not None or verifypeer is not None
        if protocol is None:
            protocol = 'https' if has_https_params else 'http'
        if port is None:
            if 'AGRAPH_PORT' in os.environ:
                port = int(os.environ.get('AGRAPH_PORT'))
            else:
                port = 10035 if protocol == 'http' else 10036
        uri = '{protocol}://{host}:{port}{tail}'.format(protocol=protocol, host=host, port=port, tail=tail)
        user = user or os.environ.get('AGRAPH_USER')
        password = password or os.environ.get('AGRAPH_PASSWORD')
        self._client = miniserver.Client(uri, user, password, cainfo, sslcert, verifyhost, verifypeer,
                                         proxy=proxy) 
    @property
    def url(self):
        """Return the server's URL."""
        return self._client.url
    @property
    def version(self):
        """Return the server's version as a string."""
        return self._client.getVersion()
[docs]    def listCatalogs(self):
        """
        Get the list of catalogs on this server.
        A value of ``None`` will be included in this list to
        represent the root catalog.
        :return: A list of catalog names.
        :rtype: list[string]
        """
        catalogs = self._client.listCatalogs()
        return catalogs 
[docs]    def openCatalog(self, name=None):
        """
        Open a catalog by name. Pass None to open the root catalog.
        :param name: One of the catalog names from :meth:`listCatalogs`
                     or ``None`` to open the root catalog.
        :return: A catalog object.
        :rtype: Catalog
        """
        # Allow for None and '' (or anything else that evaluates to False) to
        # mean the root catalog.
        if not name:
            name = None
        cats = self.listCatalogs()
        if not name in cats:
            raise ServerException("There is no catalog named '%s' (found %s)"
                % (name, cats))
        return Catalog(name, self._client) 
[docs]    def getInitfile(self):
        """
        Retrieve the contents of the server initialization file.
        The initialization file is a collection of Common Lisp code
        that is executed in every back-end as it is created.
        :return: Init file content.
        :rtype: string
        """
        return self._client.getInitfile() 
[docs]    def setInitfile(self, content=None, restart=True):
        """
        Replace the current initialization file contents with the
        ``content`` string or remove if None.
        :param content: New init file conent.
        :type content: string
        :param restart: specifies whether any running shared back-ends should
                        be shut down, so that subsequent requests will be handled by
                        back-ends that include the new code. Default is ``True``.
        """
        return self._client.setInitfile(content, restart) 
[docs]    def openSession(self, spec, autocommit=False, lifetime=None, loadinitfile=False):
        """
        Open a session on a federated, reasoning, or filtered store.
        Use the helper functions in the :mod:`franz.openrdf.sail.spec` module
        to create the spec string.
        :param spec: Session :mod:`specification string <franz.openrdf.sail.spec>`.
        :type spec: string
        :param autocommit: Autocommit mode (default: ``False`` = start a transaction).
        :type autocommit: bool
        :param lifetime: The number of seconds a session can be idle before being terminated.
                         The default value is 300 (5 minutes). The maximum acceptable value
                         is 21600 (6 hours).
        :param loadinitfile: If ``True`` the init file will be loaded into the new session.
                             The default is False.
        :type loadinitfile: bool
        :return: A connection to the new session.
        :rtype: RepositoryConnection
        """
        minirep = self._client.openSession(spec, autocommit=autocommit, lifetime=lifetime, loadinitfile=loadinitfile)
        return RepositoryConnection(Repository(None, None, minirep)) 
[docs]    def listScripts(self):
        """
        Return the list of Sitescripts currently on the server.
        When a user creates a session, they can choose to load one or more
        of these scripts into the session.
        :return: A list of script names.
        :rtype: list[string]
        """
        return self._client.listScripts() 
[docs]    def addScript(self, module, code):
        """
        Create or replace a sitescript.
        :param module: Script name.
        :type module: string
        :param code: Script content.
        :type code: string
        """
        return self._client.addScript(module, code) 
[docs]    def deleteScript(self, module):
        """
        Delete a sitescript.
        :param module: Script name.
        :type module: string
        """
        return self._client.deleteScript(module) 
[docs]    def getScript(self, module):
        """
        Get the body of a sitescript.
        :param module: Script name.
        :type module: string
        :return: Script content.
        :rtype: string
        """
        return self._client.getScript(module) 
[docs]    def openFederated(self, repositories, autocommit=False, lifetime=None, loadinitfile=False):
        """
        Open a session that federates several repositories. The
        repositories argument should be an array containing store
        designators, which can be Repository or RepositoryConnection
        objects, strings (naming a store in the root catalog, or the
        URL of a store), or (storename, catalogname) tuples.
        :param repositories: List of repositories to federate.
        :type repositories: list[string|(string, string)|Repository|RepositoryConnection]
        :param autocommit: Autocommit mode (default: ``False`` = start a transaction).
        :type autocommit: bool
        :param lifetime: The number of seconds a session can be idle before being terminated.
                         The default value is 300 (5 minutes). The maximum acceptable value
                         is 21600 (6 hours).
        :param loadinitfile: If ``True`` the init file will be loaded into the new session.
                             The default is False.
        :type loadinitfile: bool
        :return: A connection to the new session.
        :rtype: RepositoryConnection
        """
        def asRepoString(x):
            if isinstance(x, basestring): return spec.local(x)
            elif isinstance(x, tuple): return spec.local(x[0], x[1])
            elif isinstance(x, Repository): return x.getSpec()
            elif isinstance(x, RepositoryConnection): return x.getSpec()
            else: raise TypeError(str(x) + " is not a valid repository specification.")
        return self.openSession(spec.federate(*list(map(asRepoString, repositories))), autocommit, lifetime, loadinitfile) 
[docs]    def listUsers(self):
        """
        Returns a list of names of all the users that have been defined.
        """
        return self._client.listUsers() 
[docs]    def addUser(self, name, password=None):
        """
        Create a new user. Expects a password parameter, which specifies the
        user's password (can be left off when creating the anonymous user).
        """
        assert password is not None or name == 'anonymous'
        self._client.addUser(name, password) 
[docs]    def deleteUser(self, name):
        """
        Delete a user.
        """
        self._client.deleteUser(name) 
[docs]    def changeUserPassword(self, name, password):
        """
        Change the password for the given user.
        """
        self._client.changeUserPassword(name, password) 
[docs]    def listUserAccess(self, name):
        """
        Retrieve the read/write access for a user. This returns a result set,
        each element of which has a read, write, catalog, and repository
        component. The first two are booleans, the latter two strings. For
        permissions granted globally, catalog and repository will have a
        value of "*", for those granted per-catalog, only repository will
        be "*". catalog normally contains the catalog name, but for the rootAs above, but also includes the access granted to roles that this user has.
        catalog "/" is used.
        """
        return self._client.listUserAccess(name) 
[docs]    def addUserAccess(self, name, read, write, catalog='*', repository='*'):
        """
        Grant read/write access to a user.
        :param read: Whether to grant read access. Defaults to ``False``.
        :type read: bool
        :param write: Whether to grant write access. Defaults to ``False``.
        :type write: bool
        :param catalog: Which catalog to grant the access on. Leave off or pass ``"*"`` to grant
                        access on all catalogs. Use ``"/"`` for the root catalog.
        :type catalog: string
        :param repository: Specifies the repository that access is granted on. Passing ``"*"``,
                           or leaving the parameter off, means all repositories in the
                           given catalog.
        :type repository: string
        """
        self._client.addUserAccess(name, read, write, catalog, repository) 
[docs]    def deleteUserAccess(self, name, read, write, catalog='*', repository='*'):
        """
        Takes the same parameters as PUT on this URL, but revokes the access instead of granting it.
        """
        self._client.deleteUserAccess(name, read, write, catalog, repository) 
[docs]    def listUserEffectiveAccess(self, name):
        """
        Like listUserAccess, but also includes the access granted to roles that this user has.
        """
        return self._client.listUserEffectiveAccess(name) 
[docs]    def listUserPermissions(self, name):
        """
        List the permission flags that have been assigned to a user (any of
        super, eval, session, replication).
        """
        return self._client.listUserPermissions(name) 
[docs]    def listUserEffectivePermissions(self, name):
        """
        Retrieve the permission flags assigned to the user, or any of its roles.
        """
        return self._client.listUserEffectivePermissions(name) 
[docs]    def addUserPermission(self, name, _type):
        """
        Assigns the given permission to this user. type should be super, eval,
        replication, or session.
        """
        self._client.addUserPermission(name, _type) 
[docs]    def deleteUserPermission(self, name, _type):
        """
        Revokes the given permission for this user.
        """
        self._client.deleteUserPermission(name, _type) 
[docs]    def listRoles(self):
        """
        Returns the names of all defined roles.
        """
        return self._client.listRoles() 
[docs]    def addRole(self, role):
        """
        Creates a new role.
        """
        self._client.addRole(role) 
[docs]    def deleteRole(self, role):
        """
        Deletes a role.
        """
        self._client.deleteRole(role) 
[docs]    def listRolePermissions(self, role):
        """
        Lists the permission flags granted to a role.
        """
        return self._client.listRolePermissions(role) 
[docs]    def addRolePermission(self, role, _type):
        """
        Grants a role a certain permission. type should be super, eval, or session.
        """
        self._client.addRolePermission(role, _type) 
[docs]    def deleteRolePermission(self, role, _type):
        """
        Revokes a permission for a role.
        """
        self._client.deleteRolePermission(role, _type) 
[docs]    def listRoleAccess(self, role):
        """
        Query the access granted to a role. Returns a result in the same
        format as the equivalent service for users.
        """
        return self._client.listRoleAccess(role) 
[docs]    def addRoleAccess(self, role, read, write, catalog='*', repository='*'):
        """
        Grant read/write access to a role. See here for the parameters
        that are expected.
        """
        return self._client.addRoleAccess(role, read, write, catalog, repository) 
[docs]    def deleteRoleAccess(self, role, read, write, catalog='*', repository='*'):
        """
        Revoke read/write access for a role. Accepts the same parameters as above.
        """
        self._client.deleteRoleAccess(role, read, write, catalog, repository) 
[docs]    def listUserRoles(self, name):
        """
        Retrieves a list of role names for this user name.
        """
        return self._client.listUserRoles(name) 
[docs]    def addUserRole(self, name, role):
        """
        Add a role to a user.
        """
        self._client.addUserRole(name, role) 
[docs]    def deleteUserRole(self, name, role):
        """
        Remove a role from a user.
        """
        self._client.deleteUserRole(name, role) 
[docs]    def listUserSecurityFilters(self, name, _type):
        """
        List security filters for user.
        _type is one of "allow", "disallow"
        """
        return self._client.listUserSecurityFilters(name, _type) 
[docs]    def addUserSecurityFilter(self, name, _type, s=None, p=None, o=None, g=None):
        """
        Add a security filter for the user.
        name - user name
        _type - one of 'allow' or 'disallow'
        s - optional subject
        p - optional predicate
        o - optional predicate
        g - optional graph
        """
        self._client.addUserSecurityFilter(name, _type, s, p, o, g) 
[docs]    def deleteUserSecurityFilter(self, name, _type, s=None, p=None, o=None, g=None):
        """
        Add a security filter for the user.
        name - user name
        _type - one of 'allow' or 'disallow'
        s - optional subject
        p - optional predicate
        o - optional predicate
        g - optional graph
        """
        self._client.deleteUserSecurityFilter(name, _type, s, p, o, g) 
[docs]    def listRoleSecurityFilters(self, role, _type):
        """
        List security filters for user.
        _type is one of "allow", "disallow"
        """
        return self._client.listRoleSecurityFilters(role, _type) 
[docs]    def addRoleSecurityFilter(self, role, _type, s=None, p=None, o=None, g=None):
        """
        Add a security filter for the user.
        role - role name
        _type - one of 'allow' or 'disallow'
        s - optional subject
        p - optional predicate
        o - optional predicate
        g - optional graph
        """
        self._client.addRoleSecurityFilter(role, _type, s, p, o, g) 
[docs]    def deleteRoleSecurityFilter(self, role, _type, s=None, p=None, o=None, g=None):
        """
        Add a security filter for the user.
        role - role name
        _type - one of 'allow' or 'disallow'
        s - optional subject
        p - optional predicate
        o - optional predicate
        g - optional graph
        """
        self._client.deleteRoleSecurityFilter(role, _type, s, p, o, g)  
[docs]class Catalog(object):
    """
    Container of multiple repositories (triple stores).
    """
[docs]    def __init__(self, name, client):
        self.mini_catalog = client.openCatalogByName(name)
        self._name = name 
[docs]    def getName(self):
        """Return the catalog name."""
        return self._name 
    name = property(getName)
[docs]    def listRepositories(self):
        """
        Return a list of names of repositories (triple stores) managed by
        this catalog.
        """
        return self.mini_catalog.listRepositories() 
[docs]    def deleteRepository(self, name):
        """
        Delete a repository.
        :param name: Repository name.
        :type name: string
        """
        return self.mini_catalog.deleteRepository(name) 
[docs]    def getRepository(self, name, access_verb):
        """
        Creates or opens a repository.
        
        :param name: Repository name.
        :type name: string
        :param access_verb: Determines mode of operation. Possible values are:
                                - **Repository.RENEW** clears the contents of an existing
                                   repository before opening. If the indicated repository does not
                                   exist, it creates one.
                                -  **Repository.OPEN** opens an existing repository, or throws an
                                   exception if the repository is not found.
                                -  **Repository.ACCESS** opens an existing repository, or creates a
                                   new one if the repository is not found.
                                -  **Repository.CREATE** creates a new repository, or throws an
                                   exception if one by that name already exists.
        :return: A repository object.
        :rtype: Repository
        """
        access_verb = access_verb.upper()
        name = urllib.parse.quote_plus(name)
        exists = name in self.listRepositories();
        if access_verb == Repository.RENEW:
            if exists:
                self.deleteRepository(name)
            return self.createRepository(name)
        if access_verb == Repository.CREATE:
            if exists:
                raise ServerException(
                    "Can't create triple store named '%s' because a store with that name already exists.",
                    name)
            return self.createRepository(name)
        if access_verb == Repository.OPEN:
            if not exists:
                raise ServerException(
                    "Can't open a triple store named '%s' because there is none.", name)
            return Repository(self, name, self.mini_catalog.getRepository(name))
        if access_verb == Repository.ACCESS:
            if not exists:
                return self.createRepository(name)
        return Repository(self, name, self.mini_catalog.getRepository(name)) 
[docs]    def createRepository(self, name, indices=None):
        """
        Creates a new repository with the given name.
        :param name: Repository name.
        :type name: string
        :param indices: If provided, creates a store with the given indices.
                        (e.g. ``["spogi, "gspoi", ...]``)
        :type indices: list[string]
        :return: A repository object.
        :rtype: Repository
        """
        return Repository(self, name, self.mini_catalog.createRepository(name, indices=indices))