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

package com.couchbase.jdbc.core;


import com.couchbase.jdbc.CBResultSet;
import com.couchbase.jdbc.CBStatement;
import com.couchbase.jdbc.ConnectionParameters;
import com.couchbase.jdbc.connect.Cluster;
import com.couchbase.jdbc.connect.Instance;
import com.couchbase.jdbc.connect.Protocol;

import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.*;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.boon.core.reflection.MapObjectConversion;
import org.boon.json.JsonFactory;
import org.boon.json.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Created by davec on 2015-02-22.
 */
public class ProtocolImpl implements Protocol
{

    private static final String STATEMENT="statement";
    private static final String ENCODING = "encoding";
    private static final String NAMESPACE = "namespace";
    private static final String READ_ONLY = "readonly";
    private static final String TIMEOUT = "timeout";
    private static final String CREDENTIALS = "creds";
    private static final String SCAN_CONSITENCY = "scan_consistency";

    private static final int N1QL_ERROR = -1;
    private static final int N1QL_SUCCESS = 0;
    private static final int N1QL_RUNNING = 1;
    private static final int N1QL_COMPLETED = 2;
    private static final int N1QL_STOPPED = 3;
    private static final int N1QL_TIMEOUT = 4;
    private static final int N1QL_FATAL = 5;


    static final Map <String,Integer> statusStrings = new HashMap<String,Integer>();
    static {
        statusStrings.put( "errors", N1QL_ERROR );
        statusStrings.put( "success", N1QL_SUCCESS );
        statusStrings.put( "running", N1QL_RUNNING );
        statusStrings.put( "completed", N1QL_COMPLETED );
        statusStrings.put( "stopped", N1QL_STOPPED );
        statusStrings.put( "timeout", N1QL_TIMEOUT );
        statusStrings.put( "fatal", N1QL_FATAL );
    }

    String schema;
    String url;
    String user;
    String password;
    String credentials;
    String scanConsistency = "not_bounded";

    SQLWarning sqlWarning;

    Cluster cluster;
    boolean ssl=false;

    int connectTimeout=0;
    int queryTimeout=75;
    boolean readOnly = false;
    long updateCount;
    CBResultSet resultSet;
    List <String> batchStatements = new ArrayList<String>();

    public String getURL()
    {
        return url;
    }

    public String getUserName()
    {
        return user;
    }

    public String getPassword()
    {
        return password;
    }

    public String getCredentials() { return credentials; }

    public void setReadOnly( boolean readOnly )
    {
        this.readOnly = readOnly;
    }

    public boolean getReadOnly( ) { return this.readOnly; }

    private static final Logger logger = LoggerFactory.getLogger(ProtocolImpl.class);

    CloseableHttpClient httpClient;

    RequestConfig requestConfig;

    public ProtocolImpl(String url, Properties props)
    {

        if ( props.containsKey(ConnectionParameters.USER))
        {
            user=props.getProperty(ConnectionParameters.USER);
        }
        if (props.containsKey(ConnectionParameters.PASSWORD))
        {
            password=props.getProperty(ConnectionParameters.PASSWORD);
        }
        if (props.containsKey("credentials"))
        {
            credentials = props.getProperty("credentials");
        }
        this.url = url;
        setConnectionTimeout(props.getProperty(ConnectionParameters.CONNECTION_TIMEOUT));
        if (props.containsKey(ConnectionParameters.SCAN_CONSISTENCY))
        {
            scanConsistency=props.getProperty(ConnectionParameters.SCAN_CONSISTENCY);
        }

        requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(0)
                .setConnectTimeout(connectTimeout)
                .setSocketTimeout(connectTimeout)
                .build();

        if (props.containsKey(ConnectionParameters.ENABLE_SSL) && props.getProperty(ConnectionParameters.ENABLE_SSL).equals("true"))
        {
            SSLContextBuilder builder = SSLContexts.custom();

            try
            {
                builder.loadTrustMaterial(null, new TrustStrategy() {
                    @Override
                    public boolean isTrusted(X509Certificate[] chain, String authType)
                            throws CertificateException {
                        return true;
                    }
                });
                SSLContext sslContext = builder.build();
                SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                        sslContext, new X509HostnameVerifier() {
                    @Override
                    public void verify(String host, SSLSocket ssl)
                            throws IOException {
                    }

                    @Override
                    public void verify(String host, X509Certificate cert)
                            throws SSLException {
                    }

                    @Override
                    public void verify(String host, String[] cns,
                                       String[] subjectAlts) throws SSLException {
                    }

                    @Override
                    public boolean verify(String s, SSLSession sslSession) {
                        return true;
                    }
                });

                Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
                        .<ConnectionSocketFactory> create().register("https", sslsf)
                        .build();
                HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
                httpClient = HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(requestConfig).build();
                ssl=true;

            }catch (Exception ex)
            {
                logger.error("Error creating ssl client", ex);
            }



        }
        else
        {
            httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();
        }
    }

    public void connect() throws Exception
    {

        pollCluster();

    }

    private void rewriteURLs(Map<String, String> m, String sourceHost) {
        for (String key: m.keySet()) {
            String val = m.get(key);
            if (val != null && val.startsWith("http")) {
                try {
                    URL cur = new URL(val);
                    if (cur.getHost().equals("127.0.0.1")) {
                        URL revisedURL = new URL(
                                cur.getProtocol(),
                                sourceHost,
                                cur.getPort(),
                                cur.getFile());
                        m.put(key, revisedURL.toString());
                    }
                } catch (MalformedURLException e) {
                    // Not a real URL. Do nothing. Keep going.
                    continue;
                }
            }
        }
    }

    // The URLs in the JSON array may contain 127.0.0.1-based addresses.
    // If they do, we rewrite them based on the host address we used to fetch the data.
    private void rewriteURLs(List<Map> jsonArray) throws IOException {
        URL sourceURL = new URL(url);
        String sourceHost = sourceURL.getHost();
        for (Map m : jsonArray) {
           rewriteURLs(m, sourceHost);
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Cluster handleClusterResponse(CloseableHttpResponse response) throws IOException
    {
        int status = response.getStatusLine().getStatusCode();
        HttpEntity entity = response.getEntity();
        String string = EntityUtils.toString(entity);
        logger.trace ("Cluster response {}", string);
        ObjectMapper mapper = JsonFactory.create();

        // has to be an object here since we can get a 404 back which is a string
        Object jsonArray = mapper.fromJson(string);

        String message="";

        switch (status)
        {
            case 200:
                //noinspection unchecked
                rewriteURLs((List<Map>)jsonArray);
                return new Cluster((List)jsonArray, ssl);
            case 400:
                message = "Bad Request";
                break;
            case 401:
                message = "Unauthorized Request credentials are missing or invalid";
                break;
            case 403:
                message = "Forbidden Request: read only violation or client unauthorized to modify";
                break;
            case 404:
                message = "Not found: Check the URL";
                break;
            case 405:
                message = "Method not allowed: The REST method type in request is supported";
                break;
            case 409:
                message = "Conflict: attempt to create a keyspace or index that already exists";
                break;
            case 410:
                message = "Gone: The server is doing a graceful shutdown";
                break;
            case 500:
                message = "Internal server error: unforeseen problem processing the request";
                break;
            case 503:
                message = "Service Unavailable: there is an issue preventing the request from being serv serviced";
                break;
        }
        throw new ClientProtocolException(message +": " + status);

    }

    public CBResultSet query(CBStatement statement, String sql) throws SQLException
    {

        Instance instance = getNextEndpoint();


        @SuppressWarnings("unchecked") Map <String,String>parameters = new HashMap();

        parameters.put(STATEMENT,sql);
        addOptions(parameters);

        List<NameValuePair>parms = new ArrayList<NameValuePair>();

        for(String parameter:parameters.keySet())
        {
            parms.add(new BasicNameValuePair(parameter,parameters.get(parameter)));
        }


        while(true)
        {
            String url = instance.getEndpointURL(ssl);
            logger.trace("Using endpoint {}", url);

            URI uri = null;
            try
            {
                uri = new URIBuilder(url).addParameters(parms).build();
            } catch (URISyntaxException ex) {
                logger.error("Invalid request {}", url);
            }

            HttpGet httpGet = new HttpGet(uri);

            httpGet.setHeader("Accept", "application/json");
            logger.trace("Get request {}", httpGet.toString());

            try {

                CloseableHttpResponse response = httpClient.execute(httpGet);
                return new CBResultSet(statement, handleResponse(sql, response));

            } catch (ConnectTimeoutException cte)
            {
                logger.trace(cte.getLocalizedMessage());

                // this one failed, lets move on
                invalidateEndpoint(instance);
                // get the next one
                instance = getNextEndpoint();
                if (instance == null) {
                    throw new SQLException("All endpoints have failed, giving up");
                }


            } catch (IOException ex) {
                logger.error("Error executing query [{}] {}", sql, ex.getMessage());
                throw new SQLException("Error executing update", ex.getCause());
            }
        }
    }


    public int executeUpdate(CBStatement statement, String query) throws SQLException
    {
        boolean hasResultSet = execute(statement, query);
        if (!hasResultSet)
        {
            return (int)getUpdateCount();
        }
        else
        {
            return 0;
        }

    }

    public CouchResponse handleResponse(String sql, CloseableHttpResponse response) throws SQLException,IOException {
        int status = response.getStatusLine().getStatusCode();
        HttpEntity entity = response.getEntity();


        ObjectMapper mapper = JsonFactory.create();

        CouchResponse couchResponse = new CouchResponse();

        String strResponse = EntityUtils.toString(entity);
//        logger.trace( "Response to query {} {}", sql, strResponse );

        Object foo = mapper.readValue(strResponse, Map.class);
        Map<String, Object> rootAsMap = null;
        if (foo instanceof Map)
        {
            //noinspection unchecked
            rootAsMap = (Map <String,Object>) foo;
        }
        else
        {
            logger.debug("error");
        }
        couchResponse.status    = (String)rootAsMap.get("status");
        couchResponse.requestId = (String)rootAsMap.get("requestID");
        Object signature = rootAsMap.get("signature");

        if ( signature instanceof Map )
        {
            //noinspection unchecked
            couchResponse.signature = (Map)signature;
            //noinspection unchecked
            couchResponse.results   = (List)rootAsMap.get("results");
        }
        else if ( signature instanceof String )
        {
            couchResponse.signature =  new HashMap<String, String>();
            couchResponse.signature.put("$1",(String)signature);

            Iterator iterator = ((List)rootAsMap.get("results")).iterator();

            couchResponse.results=new ArrayList<>();
            while ( iterator.hasNext() )
            {
                Object object = iterator.next();

                HashMap entry = new HashMap();
                //noinspection unchecked
                entry.put("$1", object );
                //noinspection unchecked
                couchResponse.results.add(entry) ;
            }

        }
        else if (signature != null)
        {
            throw new SQLException("Error reading signature" + signature );
        }
        //noinspection unchecked
        couchResponse.metrics   = MapObjectConversion.fromMap((Map)rootAsMap.get("metrics"), CouchMetrics.class);
        List errorList = (List)rootAsMap.get("errors");
        if ( errorList != null )
        {
            //noinspection unchecked,unchecked
            couchResponse.errors    = MapObjectConversion.convertListOfMapsToObjects(CouchError.class, errorList);
        }
        List  warningList = (List)rootAsMap.get("warnings");
        if ( warningList != null )
        {
            //noinspection unchecked,unchecked
            couchResponse.warnings  = MapObjectConversion.convertListOfMapsToObjects(CouchError.class, warningList );

            for (CouchError warning : couchResponse.warnings)
            {
                if ( sqlWarning != null )
                {
                    sqlWarning = new SQLWarning(warning.msg,null, warning.code);
                }
                else
                {
                    sqlWarning.setNextWarning(new SQLWarning(warning.msg,null, warning.code));
                }
            }
        }


        //JsonObject jsonObject = jsonReader.readObject();
        //logger.trace( "response from query {} {}", sql, jsonObject.toString());


        //String statusString = (String)jsonObject.get("status");

        Integer iStatus = statusStrings.get(couchResponse.status);
        String message;

        switch (status)
        {
            case 200:
                switch (iStatus.intValue())
                {
                    case N1QL_ERROR:
                        List <CouchError> errors = couchResponse.errors;
                        throw new SQLException(errors.get(0).msg);

                    case N1QL_SUCCESS:
                        return couchResponse;

                    case N1QL_COMPLETED:
                    case N1QL_FATAL:
                    case N1QL_RUNNING:
                    case N1QL_STOPPED:
                    case N1QL_TIMEOUT:
                        message = "Invalid Status";
                        fillSQLException(message, couchResponse);

                    default:
                        logger.error("Unexpected status string {} for query {}", couchResponse.status, sql);
                        throw new SQLException("Unexpected status: " + couchResponse.status );

                }
            case 400:
                message = "Bad Request";
                fillSQLException(message, couchResponse);
            case 401:
                message = "Unauthorized Request credentials are missing or invalid";
                fillSQLException(message, couchResponse);
            case 403:
                message = "Forbidden Request: read only violation or client unauthorized to modify";
                fillSQLException(message, couchResponse);
            case 404:
                message = "Not found: Request references an invalid keyspace or there is no primary key";
                fillSQLException(message, couchResponse);
            case 405:
                message = "Method not allowed: The REST method type in request is supported";
                fillSQLException(message, couchResponse);
            case 409:
                message = "Conflict: attempt to create a keyspace or index that already exists";
                fillSQLException(message, couchResponse);
            case 410:
                message = "Gone: The server is doing a graceful shutdown";
                fillSQLException(message, couchResponse);
            case 500:
                message = "Internal server error: unforeseen problem processing the request";
                fillSQLException(message, couchResponse);
            case 503:
                message = "Service Unavailable: there is an issue preventing the request from being serviced";
                logger.debug("Error with the request {}", message);

                CouchError  errors, warnings;

                if (couchResponse.metrics.errorCount > 0 )
                {
                    errors= couchResponse.errors.get(0);
                    logger.error("Error Code: {} Message: {} for query {} ",errors.code,errors.msg, sql);
                }
                if ( couchResponse.metrics.warningCount > 0 )
                {
                    warnings = couchResponse.warnings.get(0);
                    logger.error("Warning Code: {} Message: {} for query {}",warnings.code, warnings.msg, sql);
                }


                fillSQLException(message, couchResponse);

            default:
                throw new ClientProtocolException("Unexpected response status: " + status);

        }
    }

    private void fillSQLException(String msg, CouchResponse response)  throws SQLException
    {
        CouchError error ;
        if ( response.metrics.errorCount > 0 )
        {
            error = response.errors.get(0);

        }
        else if ( response.metrics.warningCount > 0)
        {
            error = response.errors.get(0);
        }
        else
        {
            throw new SQLException(msg);
        }
        throw new SQLException(error.msg, null, error.code);
    }


    public CouchResponse doQuery(String query, Map queryParameters) throws SQLException
    {
        Instance endPoint = getNextEndpoint();

        // keep trying endpoints
        while(true)
        {

            try {
                String url = endPoint.getEndpointURL(ssl);

                logger.trace("Using endpoint {}", url);
                HttpPost httpPost = new HttpPost(url);
                httpPost.setHeader("Accept", "application/json");

                logger.trace("do query {}", httpPost.toString());
                addOptions(queryParameters);


                String jsonParameters = JsonFactory.toJson(queryParameters);
                StringEntity entity = new StringEntity(jsonParameters, ContentType.APPLICATION_JSON);


                httpPost.setEntity(entity);

                CloseableHttpResponse response = httpClient.execute(httpPost);

                return handleResponse(query, response);

            }
            catch (ConnectTimeoutException cte)
            {
                logger.trace(cte.getLocalizedMessage());

                // this one failed, lets move on
                invalidateEndpoint(endPoint);
                // get the next one
                endPoint = getNextEndpoint();
                if (endPoint == null) {
                    throw new SQLException("All endpoints have failed, giving up");
                }


            } catch (Exception ex) {
                logger.error("Error executing query [{}] {}", query, ex.getMessage());
                throw new SQLException("Error executing update", ex);
            }
        }
    }

    public boolean execute(CBStatement statement, String query) throws SQLException
    {
        try
        {
            Map parameters = new HashMap();

//            nameValuePairs.add(new BasicNameValuePair("pretty","0"));
            //noinspection unchecked
            parameters.put(STATEMENT, query);

            // do the query
            CouchResponse response = doQuery(query, parameters );

            updateCount = response.metrics.mutationCount;
            if ( updateCount > 0 )
            {
                return false;
            }

            // no sense creating the object if it is false
            if ( response.metrics.resultCount == 0 ) return false;
            resultSet = new CBResultSet(statement, response);
                return true;


        }
        catch (Exception ex)
        {
            logger.error ("Error executing update query {} {}", query, ex.getMessage());
            throw new SQLException("Error executing update",ex.getCause());
        }
    }

    public CouchResponse prepareStatement( String sql, String []returning ) throws SQLException
    {
        Map parameters = new HashMap();

//            nameValuePairs.add(new BasicNameValuePair("pretty","0"));


        // append returning clause
        if (returning != null )
        {
            sql += " RETURNING ";

            // loop through column names
            for (int i=0; i< returning.length;)
            {
                // add the column
                sql+=returning[i++];
                // add the , if not the last one
                if (i < returning.length) sql += ',';
            }
        }

        //noinspection unchecked
        parameters.put(STATEMENT, "prepare " + sql);

        return doQuery(sql, parameters);
    }

// batch statements do not work
    public int [] executeBatch() throws SQLException
    {
        try
        {
            Instance instance = getNextEndpoint();
            String url = instance.getEndpointURL(ssl);

            HttpPost httpPost = new HttpPost( url );
            httpPost.setHeader("Accept", "application/json");

            Map <String,Object> parameters = new HashMap<String,Object>();
            addOptions(parameters);
            for (String query:batchStatements)
            {
                parameters.put(STATEMENT, query);
            }

            CloseableHttpResponse response = httpClient.execute(httpPost);
            int status = response.getStatusLine().getStatusCode();

            if ( status >= 200 && status < 300 )
            {
                HttpEntity entity = response.getEntity();
                ObjectMapper mapper = JsonFactory.create();
                @SuppressWarnings("unchecked") Map <String,Object> jsonObject = (Map)mapper.fromJson(EntityUtils.toString(entity));

                String statusString = (String)jsonObject.get("status");

                if (statusString.equals("errors"))
                {
                    List errors= (List)jsonObject.get("errors");
                    Map error = (Map)errors.get(0);
                    throw new SQLException((String)error.get("msg"));
                }
                else if (statusString.equals("success"))
                {
                    Map  metrics = (Map)jsonObject.get("metrics");
                    if ( metrics.containsKey("mutationCount") )
                    {
                        updateCount = (int)metrics.get("mutationCount");
                        return new int [0];
                    }
                    if ( metrics.containsKey("resultCount") )
                    {
                       // TODO FIX ME resultSet = new CBResultSet(jsonObject);
                        return new int [0];
                    }
                }
                else if (statusString.equals("running"))
                {
                    return new int [0];
                }
                else if (statusString.equals("completed"))
                {
                    return new int [0];
                }
                else if (statusString.equals("stopped"))
                {
                    return new int [0];
                }
                else if (statusString.equals("timeout"))
                {
                    return new int [0];
                }
                else if (statusString.equals("fatal"))
                {
                    return new int [0];
                }

                else
                {
                    //logger.error("Unexpected status string {} for query {}", statusString, query);
                    throw new SQLException("Unexpected status: " + statusString);
                }
            }

            else
            {
                throw new ClientProtocolException("Unexpected response status: " + status);
            }
        }
        catch (Exception ex)
        {
            //logger.error ("Error executing update query {} {}", query, ex.getMessage());
            throw new SQLException("Error executing update",ex.getCause());
        }
        return new int [0];

    }
    public void addBatch( String query ) throws SQLException
    {
        batchStatements.add(query);
    }
    public void clearBatch()
    {
        batchStatements.clear();
    }
    public long getUpdateCount()
    {
        return updateCount;
    }
    public CBResultSet getResultSet()
    {
        return resultSet;
    }

    public void setConnectionTimeout(String timeout)
    {
        if (timeout!=null)
        {
            connectTimeout=Integer.parseInt(timeout);
        }
    }
    public void setConnectionTimeout(int timeout )
    {
        connectTimeout=timeout;
    }

    public void setQueryTimeout( int seconds ) throws SQLException
    {
        this.queryTimeout =  seconds;
    }
    public int getQueryTimeout( ) throws SQLException
    {
        return queryTimeout;
    }
    public void close() throws Exception
    {
        httpClient.close();
    }

    @Override
    public SQLWarning getWarnings() throws SQLException
    {
        return sqlWarning;
    }

    @Override
    public void clearWarning() throws SQLException
    {
       sqlWarning=null;
    }

    @Override
    public void setSchema(String schema) throws SQLException
    {
        if (schema != null && schema.compareToIgnoreCase("system")==0)
        {
            schema='#'+schema;
        }
        this.schema = schema;
    }

    @Override
    public String getSchema() throws SQLException
    {

        if (schema!= null && schema.startsWith("#"))
        {
            return schema.substring(1);
        }
        else {
            return schema;
        }
    }


    private void addOptions(Map parameters)
    {

        //noinspection unchecked
        parameters.put(ENCODING,"UTF-8");

        if ( schema != null )
        {
            //noinspection unchecked
            parameters.put(NAMESPACE, schema);
        }
        if( readOnly )
        {
            //noinspection unchecked
            parameters.put(READ_ONLY, true);
        }
        if ( queryTimeout != 0 )
        {
            //noinspection unchecked
            parameters.put(TIMEOUT, ""+queryTimeout+'s');
        }

        if (credentials != null)
        {
            //noinspection unchecked
            parameters.put(CREDENTIALS, credentials);
        }

        //noinspection unchecked
        parameters.put(SCAN_CONSITENCY,scanConsistency);

    }

    public boolean isValid(int timeout)
    {


        String query = "select 1";

        Map parameters = new HashMap();
        //noinspection unchecked
        parameters.put(STATEMENT, query);
        // do the query

        try
        {
            CouchResponse response = doQuery(query, parameters);
            return response.getMetrics().getResultCount() == 1;
        }
        catch (Exception ex)
        {
            return false;

        }
    }

    // all access to clusters has to be synchronized as there is a thread
    // changing the value
    // see CBDriver.ClusterThread

    AtomicBoolean clusterSynch = new AtomicBoolean(true);
    public Cluster getCluster()
    {
        Cluster ret=null;
        // loop waiting for access
        while (clusterSynch.getAndSet(false)){}

        ret=cluster;
        clusterSynch.set(true);
        return ret;
    }
    public void setCluster(Cluster cluster)
    {
        while(clusterSynch.getAndSet(false)){}

        this.cluster = cluster;

        clusterSynch.set(true);

    }

    public Instance getNextEndpoint()
    {
        Instance instance = null;
        while(clusterSynch.getAndSet(false)){}

        instance = cluster.getNextEndpoint();

        clusterSynch.set(true);
        return instance;
    }
    public void invalidateEndpoint(Instance instance)
    {
        while(clusterSynch.getAndSet(false)){}

        cluster.invalidateEndpoint(instance);
        clusterSynch.set(true);
    }
    public void pollCluster() throws SQLException
    {
        HttpGet httpGet = new HttpGet(url+"/admin/clusters/default/nodes");
        httpGet.setHeader("Accept", "application/json");

        try
        {
            CloseableHttpResponse httpResponse = httpClient.execute(httpGet);

            setCluster(handleClusterResponse(httpResponse));
        }
        catch(Exception ex)
        {
            logger.error("Error opening connection {}", ex.getMessage());

            throw new SQLException("Error getting cluster response", ex);}

    }
}