/*
 * Copyright (c) 2015. Rick Hightower, Geoff Chandler
 *
 * 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.
 *
 * QBit - The Microservice lib for Java : JSON, WebSocket, REST. Be The Web!
 */

package io.advantageous.qbit.service.impl;

import io.advantageous.boon.core.Str;
import io.advantageous.boon.core.reflection.BeanUtils;
import io.advantageous.qbit.Factory;
import io.advantageous.qbit.GlobalConstants;
import io.advantageous.qbit.annotation.AnnotationUtils;
import io.advantageous.qbit.client.BeforeMethodSent;
import io.advantageous.qbit.events.EventManager;
import io.advantageous.qbit.http.request.HttpRequest;
import io.advantageous.qbit.http.server.websocket.WebSocketMessage;
import io.advantageous.qbit.message.MethodCall;
import io.advantageous.qbit.message.MethodCallBuilder;
import io.advantageous.qbit.message.Request;
import io.advantageous.qbit.message.Response;
import io.advantageous.qbit.message.impl.ResponseImpl;
import io.advantageous.qbit.queue.Queue;
import io.advantageous.qbit.queue.*;
import io.advantageous.qbit.service.*;
import io.advantageous.qbit.service.health.HealthServiceAsync;
import io.advantageous.qbit.service.stats.StatsCollector;
import io.advantageous.qbit.system.QBitSystemManager;
import io.advantageous.qbit.transforms.Transformer;
import io.advantageous.qbit.util.ConcurrentHashSet;
import io.advantageous.qbit.util.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import static java.util.Arrays.stream;

/**
 * Manages a collection of services.
 *
 * NOTE NOTE NOTE NOTE.
 * NOTE if you want to debug why a method is not getting called but a break point on {@code doCall}.
 */
public class ServiceBundleImpl implements ServiceBundle {

    /**
     * Logger.
     */
    private final Logger logger = LoggerFactory.getLogger(ServiceBundleImpl.class);
    private final boolean debug = GlobalConstants.DEBUG && logger.isDebugEnabled();
    private final boolean asyncCalls;
    private final boolean invokeDynamic;
    private final QBitSystemManager systemManager;
    private final CallbackManager callbackManager;
    /**
     * Keep track of servicesToStop to forwardEvent queue mappings.
     *///SendQueue<MethodCall<Object>>
    private final Map<String, Consumer<MethodCall<Object>>> serviceMapping = new ConcurrentHashMap<>();
    /**
     * Keep a list of current servicesToStop that we are routing to.
     */
    private final Set<Stoppable> servicesToStop = new ConcurrentHashSet<>(10);

    private final Set<ServiceFlushable> servicesToFlush = new ConcurrentHashSet<>(10);
    /**
     * Keep a list of current forwardEvent queue.
     */
    private final Set<SendQueue<MethodCall<Object>>> sendQueues = new ConcurrentHashSet<>(10);
    /**
     * Method queue for receiving method calls.
     */
    private final Queue<MethodCall<Object>> methodQueue;
    /**
     *
     */
    private final SendQueue<MethodCall<Object>> methodSendQueue;
    /**
     * Response queue for returning responses from servicesToStop that we invoked.
     */
    private final Queue<Response<Object>> responseQueue;

    /**
     * Response queue for returning responses from servicesToStop that we invoked.
     */
    private final Queue<Response<Object>> webResponseQueue;

    /**
     * Base URI for servicesToStop that this bundle is managing.
     */
    private final String rootAddress;
    /**
     * Access to QBit factory.
     */
    private final Factory factory;

    /**
     * Allows interception of method calls before they get encoded by the client proxy.
     * This allows us to transform or reject method calls.
     */
    private final BeforeMethodSent beforeMethodSent;

    /**
     * Allows interception of method calls before they get sent to a client.
     * This allows us to transform or reject method calls.
     */
    private final BeforeMethodCall beforeMethodCall;
    /**
     * Allows interception of method calls before they get transformed and sent to a client.
     * This allows us to transform or reject method calls.
     */
    private final BeforeMethodCall beforeMethodCallAfterTransform;
    /**
     * Allows transformation of arguments, for example from JSON to Java objects.
     */
    private final Transformer<Request, Object> argTransformer;

    /*
     */
    private final QueueBuilder requestQueueBuilder;
    private final HealthServiceAsync healthService;
    private final StatsCollector statsCollector;
    private final Timer timer;
    private final int sampleStatFlushRate;
    private final int checkTimingEveryXCalls;
    private final EventManager eventManager;
    private final BeforeMethodCall beforeMethodCallOnServiceQueue;
    private final AfterMethodCall afterMethodCallOnServiceQueue;

    public ServiceBundleImpl(final String address,
                             final QueueBuilder requestQueueBuilder,
                             final QueueBuilder responseQueueBuilder,
                             final QueueBuilder webResponseQueueBuilder,
                             final Factory factory, final boolean asyncCalls,
                             final BeforeMethodCall beforeMethodCall,
                             final BeforeMethodCall beforeMethodCallAfterTransform,
                             final Transformer<Request, Object> argTransformer,
                             final boolean invokeDynamic,
                             final QBitSystemManager systemManager,
                             final HealthServiceAsync healthService,
                             final StatsCollector statsCollector,
                             final Timer timer,
                             final int sampleStatFlushRate,
                             final int checkTimingEveryXCalls,
                             final CallbackManager callbackManager,
                             final EventManager eventManager,
                             final BeforeMethodSent beforeMethodSent,
                             final BeforeMethodCall beforeMethodCallOnServiceQueue,
                             final AfterMethodCall afterMethodCallOnServiceQueue) {

        this.beforeMethodCallOnServiceQueue = beforeMethodCallOnServiceQueue;
        this.afterMethodCallOnServiceQueue = afterMethodCallOnServiceQueue;

        this.healthService = healthService;
        this.statsCollector = statsCollector;
        this.invokeDynamic = invokeDynamic;
        this.systemManager = systemManager;
        this.timer = timer;
        this.sampleStatFlushRate = sampleStatFlushRate;
        this.checkTimingEveryXCalls = checkTimingEveryXCalls;
        this.callbackManager = callbackManager;
        this.beforeMethodSent = beforeMethodSent;

        String rootAddress;
        if (address.endsWith("/")) {
            rootAddress = address.substring(0, address.length() - 1);
        } else {
            rootAddress = address;
        }
        this.beforeMethodCall = beforeMethodCall;
        this.beforeMethodCallAfterTransform = beforeMethodCallAfterTransform;
        this.argTransformer = argTransformer;
        this.rootAddress = rootAddress;
        this.factory = factory;
        this.asyncCalls = asyncCalls;
        this.requestQueueBuilder = requestQueueBuilder;
        this.methodQueue = requestQueueBuilder.setName("Call Queue " + address).build();
        this.responseQueue = responseQueueBuilder.setName("Response Queue " + address).build();
        this.webResponseQueue = webResponseQueueBuilder.setName("Web Response Queue " + address).build();
        this.methodSendQueue = methodQueue.sendQueueWithAutoFlush(10, TimeUnit.SECONDS);
        this.eventManager = eventManager;
    }

    /**
     * Handles calling a method
     *
     * @param methodCall method call
     */
    private void doCall(MethodCall<Object> methodCall) {
        if (debug) {
            logger.debug(ServiceBundleImpl.class.getName(), "::doCall() ",
                    methodCall.name(),
                    methodCall.address(),
                    "\n", methodCall);
        }

        try {
            if (methodCall.hasCallback()) {
                callbackManager.registerCallbacks(methodCall);
            }
            boolean[] continueFlag = new boolean[1];
            methodCall = handleBeforeMethodCall(methodCall, continueFlag);

            if (!continueFlag[0]) {
                if (debug) {
                    logger.debug(ServiceBundleImpl.class.getName() + "::doCall() " +
                            "Flag from before call handling does not want to continue");
                }
            } else {
                final Consumer<MethodCall<Object>> methodDispatcher = getMethodDispatcher(methodCall);
                methodDispatcher.accept(methodCall);

            }

        } catch (Exception ex) {
            Response<Object> response = new ResponseImpl<>(methodCall, ex);
            this.responseQueue.sendQueue().sendAndFlush(response);
        }
    }

    /**
     * Base URI for all of the servicesToStop in this bundle.
     *
     * @return base URI.
     */
    @Override
    public String address() {
        return rootAddress;
    }

    /**
     * Add a client to this bundle.
     *
     * @param object the client we want to add.
     */
    @Override
    public ServiceBundle addService(Object object) {
        if (debug) {
            logger.debug("ServiceBundleImpl::addServiceObject(object)- service added");
        }
        addServiceObject(null, object);
        return this;
    }

    /**
     * Add a client to this bundle.
     *
     * @param serviceObject the client we want to add.
     */
    @Override
    public ServiceBundle addServiceWithQueueCallBackHandlers(final Object serviceObject, final QueueCallBackHandler... queueCallBackHandlers) {
        if (debug) {
            logger.debug("ServiceBundleImpl::addServiceObject(object)- service added with handlers");
        }
        addServiceObjectWithQueueCallBackHandlers(null, serviceObject, queueCallBackHandlers);
        return this;
    }

    /**
     * Add a client to this bundle, under a certain address.
     *
     * @param serviceAddress the address of the client
     * @param serviceObject  the client we want to add.
     */
    @Override
    public ServiceBundle addServiceObject(final String serviceAddress, final Object serviceObject) {
        this.addServiceObjectWithQueueCallBackHandlers(serviceAddress, serviceObject, (QueueCallBackHandler[]) null);
        return this;
    }

    /**
     * Add a client to this bundle, under a certain address.
     *
     * @param serviceAddress        the address of the client
     * @param serviceObject         the client we want to add.
     * @param queueCallBackHandlers queue callback handler
     */
    @Override
    public ServiceBundle addServiceObjectWithQueueCallBackHandlers(final String serviceAddress,
                                                                   final Object serviceObject,
                                                                   final QueueCallBackHandler... queueCallBackHandlers) {

        logger.info("Adding service {} @ {} with object {}", ServiceBundleImpl.class.getName(),
                serviceAddress, serviceObject);

        if (serviceObject instanceof Consumer) {

            //noinspection unchecked
            addServiceConsumer(serviceAddress, (Consumer<MethodCall<Object>>) serviceObject);
            return this;
        }

        if (serviceObject instanceof ServiceQueue) {
            addServiceService(serviceAddress, (ServiceQueue) serviceObject);
            return this;
        }

        /** Turn this client object into a client with queues. */
        final ServiceBuilder serviceBuilder = ServiceBuilder.serviceBuilder()
                .setRootAddress(rootAddress)
                .setServiceObject(serviceObject)
                .setServiceAddress(serviceAddress)
                .setTimer(timer)
                .setResponseQueue(responseQueue)
                .setAsyncResponse(asyncCalls)
                .setInvokeDynamic(invokeDynamic)
                .setSystemManager(systemManager)
                .setRequestQueueBuilder(BeanUtils.copy(this.requestQueueBuilder))
                .setRequestQueueBuilder(requestQueueBuilder)
                .setHandleCallbacks(false)
                .setCreateCallbackHandler(false)
                .setEventManager(eventManager)
                .setBeforeMethodCall(this.beforeMethodCallOnServiceQueue)
                .setAfterMethodCall(this.afterMethodCallOnServiceQueue);

        if (queueCallBackHandlers != null) {
            stream(queueCallBackHandlers).forEach(serviceBuilder::addQueueCallbackHandler);
        }

        final String bindStatHealthName = serviceAddress == null
                ? AnnotationUtils.readServiceName(serviceObject)
                : serviceAddress;

        if (healthService != null) {
            serviceBuilder.registerHealthChecks(healthService, bindStatHealthName);
        }

        if (statsCollector != null) {
            /*
              The default is to flush stats every five seconds, and sample
              every 10_000 queue calls.
             */
            serviceBuilder.registerStatsCollections(bindStatHealthName,
                    statsCollector, sampleStatFlushRate, checkTimingEveryXCalls);
        }

        final ServiceQueue serviceQueue = serviceBuilder.buildAndStart();

        addServiceService(serviceAddress, serviceQueue);
        return this;
    }

    public ServiceBundle addServiceConsumer(final String serviceAddress,
                                            final Consumer<MethodCall<Object>> methodCallConsumer) {

        //noinspection UnnecessaryLocalVariable
        @SuppressWarnings("UnnecessaryLocalVariable") String address = serviceAddress;

        if (address != null && !address.isEmpty()) {
            serviceMapping.put(address, methodCallConsumer);
            serviceMapping.put(address.toLowerCase(), methodCallConsumer);

            if (methodCallConsumer instanceof ServiceFlushable) {
                this.servicesToFlush.add((ServiceFlushable) methodCallConsumer);
            }

            if (methodCallConsumer instanceof Stoppable) {
                this.servicesToStop.add((Stoppable) methodCallConsumer);
            }
        } else {
            throw new IllegalStateException("Service consumer must have an address");//TODO lookup name like you do in BoonMethodCallHandler
        }
        return this;

    }

    public void addServiceService(final String serviceAddress, final ServiceQueue serviceQueue) {

        addServiceService(null, serviceAddress, serviceQueue);
    }

    public void addServiceService(final String objectName, final String serviceAddress, final ServiceQueue serviceQueue) {

        /** add to our list of servicesToStop. */
        servicesToStop.add(serviceQueue);
        servicesToFlush.add(serviceQueue);

        QueueDispatch dispatch = new QueueDispatch(serviceQueue);

        /** Add the client given the address if we have an address. */
        if (serviceAddress != null && !serviceAddress.isEmpty()) {
            serviceMapping.put(serviceAddress, dispatch);
        }

        if (objectName != null) {
            /** Put the client incoming requests in our client name, request queue mapping. */
            serviceMapping.put(objectName, dispatch);
        }

        serviceMapping.put(serviceQueue.name().toLowerCase(), dispatch);
        serviceMapping.put(serviceQueue.name(), dispatch);
        serviceMapping.put(serviceQueue.address(), dispatch);
        serviceMapping.put(serviceQueue.address().toLowerCase(), dispatch);

        /** Add the request queue to our set of request queues. */
        sendQueues.add(dispatch.requests);

        /** Generate a list of end point addresses based on the client bundle root address. */
        final Collection<String> addresses = serviceQueue.addresses(this.rootAddress);

        if (debug) {
            logger.debug(ServiceBundleImpl.class.getName() + " addresses: " + addresses);
        }

        /** Add mappings to all addresses for this client to our serviceMapping. */
        for (String addr : addresses) {
            serviceMapping.put(addr, dispatch);
        }
    }

    /**
     * Returns a receive queue for all servicesToStop managed by this bundle.
     *
     * @return responses queue
     */
    @Override
    public Queue<Response<Object>> responses() {
        return responseQueue;
    }

    @Override
    public SendQueue<MethodCall<Object>> methodSendQueue() {
        return methodQueue.sendQueue();
    }

    /**
     * Call the method.
     */
    @Override
    @SuppressWarnings("unchecked")
    public void call(final MethodCall<Object> methodCall) {

        if (debug) {
            logger.debug(ServiceBundleImpl.class.getName() + "::call() " +
                    methodCall.name() + " " + " " +
                    methodCall.address() +
                    "\n" + methodCall);
        }
        methodSendQueue.send(methodCall);
    }

    @Override
    public void call(List<MethodCall<Object>> methodCalls) {
        if (debug) {
            logger.debug("ServiceBundleImpl::call()- methodCalls: \n" + methodCalls);
        }
        methodSendQueue.sendBatch(methodCalls);
    }

    /**
     * Creates a proxy interface to a particular client. Given a particular address.
     *
     * @param serviceInterface client view interface of client
     * @param myService        address or name of client
     * @param <T>              type of client
     * @return proxy client to client
     */
    @Override
    public <T> T createLocalProxy(final Class<T> serviceInterface, final String myService) {

        final Consumer<MethodCall<Object>> callConsumer = this.serviceMapping.get(myService);

        if (callConsumer == null) {
            logger.error("Service requested does not exist " + myService);
        }

        return factory.createLocalProxy(serviceInterface, myService, this, beforeMethodSent);

    }

    public <T> T createOneWayLocalProxy(final Class<T> serviceInterface, final String myService) {

        final Consumer<MethodCall<Object>> callConsumer = this.serviceMapping.get(myService);

        if (callConsumer == null) {
            throw new IllegalStateException("Service requested does not exist " + myService);
        }

        if (callConsumer instanceof QueueDispatch) {

            return ((QueueDispatch) callConsumer).serviceQueue.createProxyWithAutoFlush(
                    serviceInterface, 100, TimeUnit.MILLISECONDS);
        } else {
            return factory.createLocalProxy(serviceInterface, myService, this, beforeMethodSent);
        }
    }

    private MethodCall<Object> handleBeforeMethodCall(MethodCall<Object> methodCall, boolean[] continueFlag) {
        methodCall = beforeMethodCall(methodCall, continueFlag);

        return methodCall;
    }

    private Consumer<MethodCall<Object>> getMethodDispatcher(MethodCall<Object> methodCall) {
        Consumer<MethodCall<Object>> methodCallConsumer = null;

        boolean hasAddress = !Str.isEmpty(methodCall.address());

        boolean hasMethodName = !Str.isEmpty(methodCall.name());

        boolean hasObjectName = !Str.isEmpty(methodCall.objectName());

        if (hasMethodName && hasObjectName) {
            methodCallConsumer = serviceMapping.get(methodCall.objectName());
        }

        if (hasAddress && methodCallConsumer == null) {
            methodCallConsumer = getMethodDispatchByAddress(methodCall);
        }

        if (methodCallConsumer == null) {
            logger.error("No service at method address " + methodCall.address()
                    + " method name " + methodCall.name() + " object name " + methodCall.objectName() + "\n SERVICES" + serviceMapping.keySet() + "\n");

            Set<String> uris = serviceMapping.keySet();

            //noinspection CodeBlock2Expr
            uris.forEach((String it) -> {
                logger.error("known URI path " + it);
            });

            throw new ServiceMethodNotFoundException("there is no object at this address: " + methodCall.address()
                    + "\n method name=" + methodCall.name() + "\n objectName=" + methodCall.objectName(), methodCall.address());
        }
        return methodCallConsumer;
    }

    private Consumer<MethodCall<Object>> getMethodDispatchByAddress(final MethodCall<Object> methodCall) {
        Consumer<MethodCall<Object>> methodConsumerByAddress;
        final String callAddress = methodCall.address();
        methodConsumerByAddress = serviceMapping.get(callAddress);
        return methodConsumerByAddress;
    }

    /**
     * Handles before call operation
     *
     * @param methodCall   method call
     * @param continueCall should we continue the call.
     * @return call object which could have been transformed
     */
    private MethodCall<Object> beforeMethodCall(MethodCall<Object> methodCall, boolean[] continueCall) {
        if (this.beforeMethodCall.before(methodCall)) {
            continueCall[0] = true;
            methodCall = transformBeforeMethodCall(methodCall);

            continueCall[0] = this.beforeMethodCallAfterTransform.before(methodCall);
            return methodCall;

        } else {
            continueCall[0] = false;

        }
        return methodCall;
    }

    /**
     * Handles the before argument transformer.
     *
     * @param methodCall method call that we might transform
     * @return method call
     */
    private MethodCall<Object> transformBeforeMethodCall(MethodCall<Object> methodCall) {
        if (argTransformer == null || argTransformer == ServiceConstants.NO_OP_ARG_TRANSFORM) {
            return methodCall;
        }
        Object arg = this.argTransformer.transform(methodCall);
        return MethodCallBuilder.transformed(methodCall, arg);
    }

    /**
     * Flush the sends.
     */
    @Override
    public void flushSends() {

        this.methodSendQueue.flushSends();
    }

    /**
     * Stop the client bundle.
     */
    @SuppressWarnings("Convert2streamapi")
    public void stop() {
        if (debug) {
            logger.debug(ServiceBundleImpl.class.getName(), "::stop()");
        }
        methodQueue.stop();
        for (Stoppable service : servicesToStop) {
            service.stop();
        }

        try {
            responseQueue.stop();
        } catch (Exception ex) {
            logger.debug("", ex);
        }

        try {
            webResponseQueue.stop();
        } catch (Exception ex) {
            logger.debug("", ex);
        }
        if (systemManager != null) systemManager.serviceShutDown();
    }

    /**
     * Return a list of end points that we are handling.
     */
    @Override
    public List<String> endPoints() {
        return new ArrayList<>(serviceMapping.keySet());
    }

    public void startReturnHandlerProcessor(ReceiveQueueListener<Response<Object>> listener) {

        responseQueue.startListener(listener);

    }

    public void startWebResponseReturnHandler(ReceiveQueueListener<Response<Object>> listener) {

        webResponseQueue.startListener(listener);

    }

    /**
     * Handles responses coming back from services.
     */
    public void startReturnHandlerProcessor() {

        final SendQueue<Response<Object>> webResponseSendQueue = webResponseQueue.sendQueue();

        responseQueue.startListener(new ReceiveQueueListener<Response<Object>>() {

            @Override
            public void receive(Response<Object> response) {

                final Request<Object> originatingRequest = response.request().originatingRequest();

                if (originatingRequest == null) {
                    callbackManager.handleResponse(response);
                } else if (originatingRequest instanceof HttpRequest || originatingRequest instanceof WebSocketMessage) {
                    webResponseSendQueue.send(response);
                } else {
                    callbackManager.handleResponse(response);
                }
            }

            @Override
            public void empty() {
                webResponseSendQueue.flushSends();

            }

            @Override
            public void limit() {
                webResponseSendQueue.flushSends();

            }

            @Override
            public void shutdown() {

            }

            @Override
            public void idle() {
                webResponseSendQueue.flushSends();

            }

            @Override
            public void startBatch() {

            }
        });
    }

    /**
     * Start the client bundle.
     */
    public ServiceBundle startUpCallQueue() {
        methodQueue.startListener(new ReceiveQueueListener<MethodCall<Object>>() {

            long time;

            long lastTimeAutoFlush;

            /**
             * When we receive a method call, we call doCall.
             * @param item item
             */
            @Override
            public void receive(MethodCall<Object> item) {
                doCall(item); //Do call calls forwardEvent but does not flush. Only when the queue is empty do we flush.
            }

            /**
             * If the queue is empty, then go ahead, and flush to each client all incoming requests every 50 milliseconds.
             */
            @Override
            public void empty() {
                time = timer.now();
                if (time > (lastTimeAutoFlush + 50)) {

                    //noinspection Convert2streamapi
                    for (SendQueue<MethodCall<Object>> sendQueue : sendQueues) {
                        sendQueue.flushSends();
                    }
                    lastTimeAutoFlush = time;
                }
            }

        });

        return this;
    }

    @SuppressWarnings("Convert2streamapi")
    public void flush() {

        flushSends();
        for (ServiceFlushable service : servicesToFlush) {
            service.flush();
        }

    }

    public ServiceBundle addServiceQueue(String objectName, ServiceQueue serviceQueue) {

        final String objectAddress = objectName.startsWith("/") ? this.address() + objectName :
                Str.add(this.address(), "/", objectName);

        addServiceService(objectName, objectAddress, serviceQueue);
        return this;
    }

    static class QueueDispatch implements Consumer<MethodCall<Object>> {

        final ServiceQueue serviceQueue;
        final SendQueue<MethodCall<Object>> requests;

        QueueDispatch(ServiceQueue serviceQueue) {
            this.serviceQueue = serviceQueue;
            /* Create an forwardEvent queue for this client. which we access from a single thread. */
            requests = serviceQueue.requests();

        }

        @Override
        public void accept(MethodCall<Object> objectMethodCall) {
            requests.send(objectMethodCall);
        }
    }

}


package io.advantageous.qbit.service.discovery.dns;

import io.advantageous.qbit.reactive.Callback;
import io.advantageous.qbit.service.discovery.EndpointDefinition;
import io.vertx.core.dns.DnsClient;
import io.vertx.core.dns.SrvRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * DNS Support for service discovery.
 * This looks up DNS entries for a given domain name.
 *

 * It has two main methods.
 *

 * One method allow you to look up things by URL e.g., db.skydns.local. .
 *

 * The other method allows you to look things up by QBit service name e.g., dbService.
 */
public class DnsSupport {

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

    private final boolean debug = logger.isDebugEnabled();

    /**
     * Holds mappings from DNS names to service names.
     */
    private final Map<String, String> dnsServiceNameToServiceName;

    /**
     * Holds mappings from service names to dns names.
     */
    private final Map<String, String> serviceNameToDNSName;

    /**
     * Holds a postfixURL to hold what URL comes after the service name.
     *

     * Example: db.skydns.local.
     * In the above db is the service and skydns.local. is the postfix URL.
     *

     * The postfixURL equates to the name in the SRV DNS record.
     */
    private final String postfixURL;

    /**
     * Class that knows how to create an instance of DnsClient.
     */
    private final DnsClientSupplier dnsClientProvider;

    /**
     * @param dnsClientProvider           dnsClientProvider
     * @param dnsServiceNameToServiceName dnsServiceNameToServiceName
     * @param postFixURL                  postFixURL
     */
    public DnsSupport(final DnsClientSupplier dnsClientProvider,
                      final Map<String, String> dnsServiceNameToServiceName,
                      final String postFixURL) {

        this.dnsClientProvider = dnsClientProvider;
        this.postfixURL = postFixURL == null ? "" : postFixURL;
        this.dnsServiceNameToServiceName = dnsServiceNameToServiceName;
        this.serviceNameToDNSName = new HashMap<>(dnsServiceNameToServiceName.size());

        /*
         * Build serviceNameToDNSName by reversing the dnsServiceNameToServiceName mappings.
         */
        dnsServiceNameToServiceName.entrySet().forEach(entry -> serviceNameToDNSName.put(entry.getValue(), entry.getKey()));

        if (debug) {
            logger.debug("DnsSupport dnsClientProvider={} postfixURL={} dnsServiceNameToServiceName={} serviceNameToDNSName={}",
                    this.dnsClientProvider, this.postfixURL, this.dnsServiceNameToServiceName, this.serviceNameToDNSName);
        }
    }

    /**
     * Looks up a service name based on its dns service name. The service part of the SRV DNS Record.
     *
     * @param dnsServiceName dnsServiceName
     * @return serviceName
     */
    public String findServiceName(final String dnsServiceName) {
        String serviceName = dnsServiceNameToServiceName.get(dnsServiceName);
        serviceName = serviceName == null ? dnsServiceName : serviceName;

        if (debug) logger.debug("FindServiceName dnsServiceName={} serviceName={}", dnsServiceName, serviceName);

        return serviceName;
    }

    /**
     * Looks up a dns service name (SRV DNS RECORD).
     *
     * @param serviceName serviceName
     * @return DNS service name (server field + name of SRV DNS Record).
     */
    public String findDnsServiceName(final String serviceName) {
        String dnsServiceName = serviceNameToDNSName.get(serviceName);
        dnsServiceName = (dnsServiceName == null ? serviceName : dnsServiceName) + postfixURL;

        if (debug) logger.debug("Find DNS_ServiceName dnsServiceName={} serviceName={}", dnsServiceName, serviceName);
        return dnsServiceName;
    }

    /**
     * Load the service nodes based on the internal service name.
     * DB, Ingester, RadarAggregator, etc.
     *
     * @param callback    callback
     * @param serviceName serviceName
     */
    public void loadServiceEndpointsByServiceName(final Callback<List<EndpointDefinition>> callback,
                                                  final String serviceName) {

        loadServiceEndpointsByDNSService(callback, findDnsServiceName(serviceName));
    }

    /**
     * Load the services nodes by its "${SRV.service}${SRV.name}".
     *
     * @param callback   callback
     * @param serviceURL serviceURL
     */
    public void loadServiceEndpointsByDNSService(final Callback<List<EndpointDefinition>> callback,
                                                 final String serviceURL) {
        final DnsClient dnsClient = dnsClientProvider.get();
        dnsClient.resolveSRV(serviceURL, event ->
                {
                    if (event.succeeded()) {
                        if (debug) logger.debug("loadServiceEndpointsByDNSService SUCCESS serviceURL={} ", serviceURL);
                        callback.resolve(convertEndpoints(event.result()));
                    } else {

                        Throwable error = event.cause();

                        logger.info("loadServiceEndpointsByDNSService FAILURE  " + serviceURL, error);

                        attemptRecover(callback, serviceURL, error);

                    }
                }
        );
    }

    private void attemptRecover(final Callback<List<EndpointDefinition>> callback, final String serviceURL, final Throwable error) {
        final DnsClient dnsClient2 = dnsClientProvider.getIfErrors();

        dnsClient2.resolveSRV(serviceURL, event -> {

            if (event.succeeded()) {

                if (debug)
                    logger.debug("loadServiceEndpointsByDNSService FAIL OVER SUCCESS serviceURL={} ", serviceURL);
                callback.resolve(convertEndpoints(event.result()));
            } else {

                logger.info("loadServiceEndpointsByDNSService FAIL OVER FAILURE  " + serviceURL, event.cause());

                //Send the first failure
                callback.onError(error);
            }
        });
    }

    /**
     * Converts list of SrvRecord(s) to list of EndpointDefinition(s).
     *
     * @param results of SrvRecord to convert to EndpointDefinition(s)
     * @return list of EndpointDefinition
     */
    private List<EndpointDefinition> convertEndpoints(final List<SrvRecord> results) {
        return results.stream().map(this::convertSrvRecordToEndpointDefinition
        ).collect(Collectors.<EndpointDefinition>toList());
    }

    /**
     * Convert a single srvRecord into an EndpointDefinition.
     *
     * @param srvRecord srvRecord
     * @return EndpointDefinition from srvRecord
     */
    private EndpointDefinition convertSrvRecordToEndpointDefinition(final SrvRecord srvRecord) {
        return new EndpointDefinition(findServiceName(srvRecord.service()), srvRecord.target(),
                srvRecord.port());
    }

}


package io.advantageous.qbit.http.server;

import io.advantageous.qbit.annotation.RequestMethod;
import io.advantageous.qbit.http.config.CorsSupport;
import io.advantageous.qbit.http.request.decorator.HttpBinaryResponseHolder;
import io.advantageous.qbit.http.request.decorator.HttpResponseDecorator;
import io.advantageous.qbit.http.request.decorator.HttpTextResponseHolder;
import io.advantageous.qbit.util.MultiMap;
import io.advantageous.qbit.util.MultiMapImpl;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created by cmathias on 8/13/15.
 *

 * Ideas borrowed largely from Tomcat 7 CORS Servlet impl.
 */
public class CorsResponseDecorator implements HttpResponseDecorator {

    /**
     * The Access-Control-Allow-Origin header indicates whether a resource can
     * be shared based by returning the value of the Origin request header in
     * the response.
     */
    public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN =
            "Access-Control-Allow-Origin";
    /**
     * The Access-Control-Allow-Credentials header indicates whether the
     * response to request can be exposed when the omit credentials flag is
     * unset. When part of the response to a preflight request it indicates that
     * the actual request can include user credentials.
     */
    public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS =
            "Access-Control-Allow-Credentials";
    /**
     * The Access-Control-Expose-headers header indicates which headers are safe
     * to expose to the API of a CORS API specification
     */
    public static final String RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS =
            "Access-Control-Expose-headers";
    /**
     * The Access-Control-Max-Age header indicates how long the results of a
     * preflight request can be cached in a preflight result cache.
     */
    public static final String RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE =
            "Access-Control-Max-Age";
    /**
     * The Access-Control-Allow-Methods header indicates, as part of the
     * response to a preflight request, which methods can be used during the
     * actual request.
     */
    public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS =
            "Access-Control-Allow-Methods";
    /**
     * The Access-Control-Allow-headers header indicates, as part of the
     * response to a preflight request, which header field names can be used
     * during the actual request.
     */
    public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS =
            "Access-Control-Allow-headers";
    /**
     * The Origin header indicates where the cross-origin request or preflight
     * request originates from.
     */
    public static final String REQUEST_HEADER_ORIGIN = "Origin";
    /**
     * The Access-Control-Request-Method header indicates which method will be
     * used in the actual request as part of the preflight request.
     */
    public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD =
            "Access-Control-Request-Method";
    /**
     * The Access-Control-Request-headers header indicates which headers will be
     * used in the actual request as part of the preflight request.
     */
    public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS =
            "Access-Control-Request-headers";
    private static final Logger log = Logger.getLogger("CorsService");
    private final CorsSupport corsSupport;
    /**
     * Determines if any origin is allowed to make request.
     */
    private boolean anyOriginAllowed;
    /**
     * Indicates (in seconds) how long the results of a pre-flight request can
     * be cached in a pre-flight result cache.
     */
    private long preFlightMaxAge = 1800;

    public CorsResponseDecorator(CorsSupport corsSupport) {
        this.corsSupport = corsSupport;
        this.anyOriginAllowed = corsSupport.getAllowedOrigins().contains("*");
    }

    /**
     * Joins elements of {@link Set} into a string, where each element is
     * separated by the provided separator.
     *
     * @param elements      The {@link Set} containing elements to join together.
     * @param joinSeparator The character to be used for separating elements.
     * @return The joined {@link String}; null if elements
     * {@link Set} is null.
     */
    protected static String join(final Collection<String> elements,
                                 final String joinSeparator) {
        String separator = ",";
        if (elements == null) {
            return null;
        }
        if (joinSeparator != null) {
            separator = joinSeparator;
        }
        StringBuilder buffer = new StringBuilder();
        boolean isFirst = true;
        for (String element : elements) {
            if (!isFirst) {
                buffer.append(separator);
            } else {
                isFirst = false;
            }

            if (element != null) {
                buffer.append(element);
            }
        }

        return buffer.toString();
    }

    /**
     * Checks if a given origin is valid or not. Criteria:
     *

     *
If an encoded character is present in origin, it's not valid.

     *
If origin is "null", it's valid.

     *
Origin should be a valid {@link URI}

     *

     *
     * @param origin origin
     * @return if this is a valid origin
     * @see RFC952
     */
    protected static boolean isValidOrigin(String origin) {
        // Checks for encoded characters. Helps prevent CRLF injection.
        if (origin.contains("%")) {
            return false;
        }

        // "null" is a valid origin
        if ("null".equals(origin)) {
            return true;
        }

        URI originURI;

        try {
            originURI = new URI(origin);
        } catch (URISyntaxException e) {
            return false;
        }
        // If scheme for URI is null, return false. Return true otherwise.
        return originURI.getScheme() != null;

    }

    // -------------------------------------------------- CORS Response headers

    @Override
    public boolean decorateTextResponse(HttpTextResponseHolder responseHolder, String requestPath, String requestMethod, int code, String contentType, String payload, MultiMap<String, String> responseHeaders, MultiMap<String, String> requestHeaders, MultiMap<String, String> requestParams) {
        boolean passedCorsCheck = checkCorsAndContinue(
                new HttpRequestHolder(
                        contentType,
                        requestMethod,
                        payload.getBytes(),
                        requestPath,
                        requestHeaders,
                        requestParams),
                new HttpResponseHolder(responseHeaders)
        );

        return passedCorsCheck;
    }

    @Override
    public boolean decorateBinaryResponse(HttpBinaryResponseHolder responseHolder, String requestPath, String requestMethod, int code, String contentType, byte[] payload, MultiMap<String, String> responseHeaders, MultiMap<String, String> requestHeaders, MultiMap<String, String> requestParams) {
        boolean passedCorsCheck = checkCorsAndContinue(
                new HttpRequestHolder(
                        contentType,
                        requestMethod,
                        payload,
                        requestPath,
                        requestHeaders,
                        requestParams),
                new HttpResponseHolder(responseHeaders)
        );

        return passedCorsCheck;
    }

    private boolean checkCorsAndContinue(HttpRequestHolder requestHolder, final HttpResponseHolder responseHolder) {

        // Determines the CORS request type.
        CorsResponseDecorator.CORSRequestType requestType = checkRequestType(requestHolder);

        switch (requestType) {
            case SIMPLE:
                // Handles a Simple CORS request.
                return this.handleSimpleCORS(requestHolder, responseHolder);
            case ACTUAL:
                // Handles an Actual CORS request.
                return this.handleSimpleCORS(requestHolder, responseHolder);
            case PRE_FLIGHT:
                // Handles a Pre-flight CORS request.
                return this.handlePreflightCORS(requestHolder, responseHolder);
            case NOT_CORS:
                // Handles a Normal request that is not a cross-origin request.
                return true;
            default:
                // Handles a CORS request that violates specification.
                return this.handleInvalidCORS(requestHolder, responseHolder);
        }
    }

    /**
     * Handles a CORS request of type {@link CORSRequestType}.SIMPLE.
     *
     * @param request  request
     * @param response response
     * @return is there a CORS simple handler for this request or response.
     */
    protected boolean handleSimpleCORS(final HttpRequestHolder request,
                                       final HttpResponseHolder response) {

        CorsResponseDecorator.CORSRequestType requestType = checkRequestType(request);
        if (!(requestType == CorsResponseDecorator.CORSRequestType.SIMPLE ||
                requestType == CorsResponseDecorator.CORSRequestType.ACTUAL)) {
            throw new IllegalArgumentException(CorsSupport.CORS_WRONG_TYPE_2);//TODO: String replacement
        }

        final String origin = request.getHeaders().get(CorsResponseDecorator.REQUEST_HEADER_ORIGIN);
        final RequestMethod method = RequestMethod.valueOf(request.getMethod());

        // Section 6.1.2
        if (!isOriginAllowed(origin)) {
            handleInvalidCORS(request, response);
            return false;
        }

        if (!corsSupport.getAllowedMethods().contains(method)) {
            handleInvalidCORS(request, response);
            return false;
        }

        // Section 6.1.3
        // Add a single Access-Control-Allow-Origin header.
        if (anyOriginAllowed && !corsSupport.isAllowCredentials()) {
            // If resource doesn't support credentials and if any origin is
            // allowed
            // to make CORS request, return header with '*'.
            response.getHeaders().add(
                    CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
                    "*");
        } else {
            // If the resource supports credentials add a single
            // Access-Control-Allow-Origin header, with the value of the Origin
            // header as value.
            response.getHeaders().add(
                    CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
                    origin);
        }

        // Section 6.1.3
        // If the resource supports credentials, add a single
        // Access-Control-Allow-Credentials header with the case-sensitive
        // string "true" as value.
        if (corsSupport.isAllowCredentials()) {
            response.getHeaders().add(
                    CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
                    "true");
        }

        // Section 6.1.4
        // If the list of exposed headers is not empty add one or more
        // Access-Control-Expose-headers headers, with as values the header
        // field names given in the list of exposed headers.
        if ((corsSupport.getExposedHeaders() != null) && (corsSupport.getExposedHeaders().size() > 0)) {
            String exposedHeadersString = join(corsSupport.getExposedHeaders(), ",");
            response.getHeaders().add(
                    CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
                    exposedHeadersString);
        }

        return true;
    }

    /**
     * Handles CORS pre-flight request.
     *
     * @param request  request
     * @param response response
     * @return return true if pre flight handler.
     */
    protected boolean handlePreflightCORS(final HttpRequestHolder request,
                                          final HttpResponseHolder response) {

        CORSRequestType requestType = checkRequestType(request);
        if (requestType != CORSRequestType.PRE_FLIGHT) {
            throw new IllegalArgumentException(CorsSupport.CORS_WRONG_TYPE_2);
            //TODO: String replace into above CORSRequestType.PRE_FLIGHT.name().toLowerCase(Locale.ENGLISH)));
        }

        final String origin = request.getHeaders().get(CorsResponseDecorator.REQUEST_HEADER_ORIGIN);

        // Section 6.2.2
        if (!isOriginAllowed(origin)) {
            handleInvalidCORS(request, response);
            return false;
        }

        // Section 6.2.3
        String accessControlRequestMethod = request.getHeaders().get(
                CorsResponseDecorator.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD);
        if (accessControlRequestMethod == null) {
            handleInvalidCORS(request, response);
            return false;
        } else {
            accessControlRequestMethod = accessControlRequestMethod.trim();
        }

        // Section 6.2.4
        String accessControlRequestHeadersHeader = request.getHeaders().get(
                CorsResponseDecorator.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS);
        List<String> accessControlRequestHeaders = new LinkedList<>();
        if (accessControlRequestHeadersHeader != null &&
                !accessControlRequestHeadersHeader.trim().isEmpty()) {
            String[] headers = accessControlRequestHeadersHeader.trim().split(
                    ",");
            for (String header : headers) {
                accessControlRequestHeaders.add(header.trim().toLowerCase(Locale.ENGLISH));
            }
        }

        // Section 6.2.5
        if (!corsSupport.getAllowedHeaders().contains(accessControlRequestMethod)) {
            handleInvalidCORS(request, response);
            return false;
        }

        // Section 6.2.6
        if (!accessControlRequestHeaders.isEmpty()) {
            for (String header : accessControlRequestHeaders) {
                if (!corsSupport.getAllowedHeaders().contains(header)) {
                    handleInvalidCORS(request, response);
                    return false;
                }
            }
        }

        // Section 6.2.7
        if (corsSupport.isAllowCredentials()) {
            response.getHeaders().add(
                    CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
                    origin);
            response.getHeaders().add(
                    CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
                    "true");
        } else {
            if (anyOriginAllowed) {
                response.getHeaders().add(
                        CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
                        "*");
            } else {
                response.getHeaders().add(
                        CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
                        origin);
            }
        }

        // Section 6.2.8
        if (preFlightMaxAge > 0) {
            response.getHeaders().add(
                    CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE,
                    String.valueOf(preFlightMaxAge));
        }

        // Section 6.2.9
        response.getHeaders().add(
                CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS,
                accessControlRequestMethod);

        // Section 6.2.10
        if ((corsSupport.getAllowedHeaders() != null) && (!corsSupport.getAllowedHeaders().isEmpty())) {
            response.getHeaders().add(
                    CorsResponseDecorator.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
                    join(corsSupport.getAllowedHeaders(), ","));
        }

        return true;

    }

    /**
     * Handles a CORS request that violates specification.
     */
    private boolean handleInvalidCORS(final HttpRequestHolder request,
                                      final HttpResponseHolder response) {
        String origin = request.getHeaders().get(CorsResponseDecorator.REQUEST_HEADER_ORIGIN);
        String method = request.getMethod();
        String accessControlRequestHeaders = request.getHeaders().get(
                REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS);

        response.getHeaders().put("Content-Type", "text/plain");

        //TODO: Rick note that the integration-point implementation still prevents me from decorating the http response code
        //response.code = HttpStatus.SC_FORBIDDEN;

        if (log.getLevel() == Level.FINE) {
            // Debug so no need for i18n
            StringBuilder message =
                    new StringBuilder("Invalid CORS request; Origin=");
            message.append(origin);
            message.append(";Method=");
            message.append(method);
            if (accessControlRequestHeaders != null) {
                message.append(";Access-Control-Request-headers=");
                message.append(accessControlRequestHeaders);
            }
            log.fine(message.toString());
        }

        return false;
    }

    // -------------------------------------------------- CORS Request headers

    /**
     * Determines the request type.
     *
     * @param request request
     * @return CORs request type.
     */
    protected CORSRequestType checkRequestType(final HttpRequestHolder request) {
        CORSRequestType requestType = CORSRequestType.INVALID_CORS;
        if (request == null) {
            throw new IllegalArgumentException(CorsSupport.CORS_NULL_REQUEST);
        }
        String originHeader = request.getHeaders().get(REQUEST_HEADER_ORIGIN);
        // Section 6.1.1 and Section 6.2.1
        if (originHeader != null) {
            if (originHeader.isEmpty()) {
                requestType = CORSRequestType.INVALID_CORS;
            } else if (!isValidOrigin(originHeader)) {
                requestType = CORSRequestType.INVALID_CORS;
            } else if (isLocalOrigin(request, originHeader)) {
                return CORSRequestType.NOT_CORS;
            } else {
                String method = request.getMethod();
                if (method != null) {
                    if ("OPTIONS".equals(method)) {
                        String accessControlRequestMethodHeader =
                                request.getHeaders().get(
                                        REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD);
                        if (accessControlRequestMethodHeader != null &&
                                !accessControlRequestMethodHeader.isEmpty()) {
                            requestType = CORSRequestType.PRE_FLIGHT;
                        } else if (accessControlRequestMethodHeader != null &&
                                accessControlRequestMethodHeader.isEmpty()) {
                            requestType = CORSRequestType.INVALID_CORS;
                        } else {
                            requestType = CORSRequestType.ACTUAL;
                        }
                    } else if ("GET".equals(method) || "HEAD".equals(method)) {
                        requestType = CORSRequestType.SIMPLE;
                    } else if ("POST".equals(method)) {
                        String mediaType = request.getContentType();
                        if (mediaType != null) {
//                                if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES
//                                        .contains(mediaType)) {
//                                    requestType = CORSRequestType.SIMPLE;
//                                } else {
//                                    requestType = CORSRequestType.ACTUAL;
//                                }

                            //TODO: Will QBit support non-simple?
                            requestType = CORSRequestType.SIMPLE;
                        }
                    } else {
                        requestType = CORSRequestType.ACTUAL;
                    }
                }
            }
        } else {
            requestType = CORSRequestType.NOT_CORS;
        }

        return requestType;
    }

    private boolean isLocalOrigin(HttpRequestHolder request, String origin) {

        // Build scheme://host:port from request
        StringBuilder target = new StringBuilder();
        URI uri = URI.create(request.getRequestUri());

        String scheme = uri.getScheme();
        if (scheme == null) {
            return false;
        } else {
            scheme = scheme.toLowerCase(Locale.ENGLISH);
        }
        target.append(scheme);
        target.append("://");

        String host = uri.getHost();
        if (host == null) {
            return false;
        }
        target.append(host);

        int port = uri.getPort();
        if ("http".equals(scheme) && port != 80 ||
                "https".equals(scheme) && port != 443) {
            target.append(':');
            target.append(port);
        }

        return origin.equalsIgnoreCase(target.toString());
    }

    /**
     * Checks if the Origin is allowed to make a CORS request.
     *
     * @param origin The Origin.
     * @return true if origin is allowed; false
     * otherwise.
     */
    private boolean isOriginAllowed(final String origin) {
        if (anyOriginAllowed) {
            return true;
        }

        // If 'Origin' header is a case-sensitive match of any of allowed
        // origins, then return true, else return false.
        return corsSupport.getAllowedOrigins().contains(origin);
    }

    // -------------------------------------------------------------- Constants

    /**
     * Enumerates varies types of CORS requests. Also, provides utility methods
     * to determine the request type.
     */
    protected enum CORSRequestType {
        /**
         * A simple HTTP request, i.e. it shouldn't be pre-flighted.
         */
        SIMPLE,
        /**
         * A HTTP request that needs to be pre-flighted.
         */
        ACTUAL,
        /**
         * A pre-flight CORS request, to get meta information, before a
         * non-simple HTTP request is sent.
         */
        PRE_FLIGHT,
        /**
         * Not a CORS request, but a normal request.
         */
        NOT_CORS,
        /**
         * An invalid CORS request, i.e. it qualifies to be a CORS request, but
         * fails to be a valid one.
         */
        INVALID_CORS
    }

    class HttpRequestHolder {
        private String contentType;
        private String method;
        private byte[] payload;
        private String requestUri;
        private MultiMap<String, String> headers;
        private MultiMap<String, String> params;

        public HttpRequestHolder(String contentType, String requestMethod, byte[] payload, String requestUri, MultiMap<String, String> Headers, MultiMap<String, String> params) {
            this.contentType = contentType;
            this.method = requestMethod;
            this.payload = payload;
            this.requestUri = requestUri;
            this.headers = Headers;
            this.params = params;
        }

        public String getContentType() {
            return contentType;
        }

        public String getMethod() {
            return method;
        }

        public byte[] getPayload() {
            return payload;
        }

        public String getRequestUri() {
            return requestUri;
        }

        public MultiMap<String, String> getHeaders() {
            if (headers == null) {
                headers = new MultiMapImpl<>();
            }
            return headers;
        }

        public MultiMap<String, String> getParams() {
            if (params == null) {
                params = new MultiMapImpl<>();
            }
            return params;
        }
    }

    class HttpResponseHolder {
        private MultiMap<String, String> headers;

        public HttpResponseHolder(MultiMap<String, String> headers) {
            this.headers = headers;
        }

        public MultiMap<String, String> getHeaders() {
            if (headers == null) {
                headers = new MultiMapImpl<>();
            }
            return headers;
        }
    }
}


/*
 * Copyright (c) 2015. Rick Hightower, Geoff Chandler
 *
 * 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.
 *
 * QBit - The Microservice lib for Java : JSON, WebSocket, REST. Be The Web!
 */

package io.advantageous.qbit.vertx.http.server;

import io.advantageous.boon.core.Str;
import io.advantageous.boon.core.Sys;
import io.advantageous.boon.core.reflection.BeanUtils;
import io.advantageous.qbit.GlobalConstants;
import io.advantageous.qbit.http.HttpContentTypes;
import io.advantageous.qbit.http.config.HttpServerOptions;
import io.advantageous.qbit.http.request.HttpRequest;
import io.advantageous.qbit.http.request.HttpResponseCreator;
import io.advantageous.qbit.http.request.decorator.HttpResponseDecorator;
import io.advantageous.qbit.http.server.HttpServer;
import io.advantageous.qbit.http.server.RequestContinuePredicate;
import io.advantageous.qbit.http.server.impl.SimpleHttpServer;
import io.advantageous.qbit.http.server.websocket.WebSocketMessage;
import io.advantageous.qbit.http.websocket.WebSocket;
import io.advantageous.qbit.service.discovery.ServiceDiscovery;
import io.advantageous.qbit.service.health.HealthServiceAsync;
import io.advantageous.qbit.system.QBitSystemManager;
import io.advantageous.qbit.util.Timer;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.ServerWebSocket;
import io.vertx.core.net.JksOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 */
public class HttpServerVertx implements HttpServer {

    private final Logger logger = LoggerFactory.getLogger(HttpServerVertx.class);
    private final boolean debug = GlobalConstants.DEBUG || logger.isDebugEnabled();
    private final QBitSystemManager systemManager;
    private final SimpleHttpServer simpleHttpServer;
    private final int port;
    private final String host;
    private final Vertx vertx;
    private final HttpServerOptions options;
    private final VertxServerUtils vertxUtils = new VertxServerUtils();
    private final boolean startedVertx;
    private io.vertx.core.http.HttpServer httpServer;

    /**
     * Holds on to Boon cache so we don't have to recreate reflected gak.
     */
    private Object context = Sys.contextToHold();

    /**
     * For Metrics.
     */
    private volatile int exceptionCount;
    /**
     * For Metrics.
     */
    private volatile int closeCount;

    public HttpServerVertx(final boolean startedVertx,
                           final Vertx vertx,
                           final String endpointName,
                           final HttpServerOptions options,
                           final QBitSystemManager systemManager,
                           final ServiceDiscovery serviceDiscovery,
                           final HealthServiceAsync healthServiceAsync,
                           final int serviceDiscoveryTtl,
                           final TimeUnit serviceDiscoveryTtlTimeUnit,
                           final CopyOnWriteArrayList<HttpResponseDecorator> decorators,
                           final HttpResponseCreator httpResponseCreator,
                           final RequestContinuePredicate requestBodyContinuePredicate) {

        this.startedVertx = startedVertx;

        this.simpleHttpServer = new SimpleHttpServer(endpointName, systemManager,
                options.getFlushInterval(), options.getHost(), options.getPort(), serviceDiscovery,
                healthServiceAsync, serviceDiscoveryTtl, serviceDiscoveryTtlTimeUnit,
                decorators, httpResponseCreator, requestBodyContinuePredicate);
        this.vertx = vertx;
        this.systemManager = systemManager;
        this.port = options.getPort();
        this.host = options.getHost();
        this.options = BeanUtils.copy(options);
        this.setWebSocketIdleConsume(aVoid -> {
        });
        this.setHttpRequestsIdleConsumer(aVoid -> {
        });
    }

    @Override
    public void setShouldContinueHttpRequest(final Predicate<HttpRequest> shouldContinueHttpRequest) {
        this.simpleHttpServer.setShouldContinueHttpRequest(shouldContinueHttpRequest);
    }

    @Override
    public void setWebSocketMessageConsumer(final Consumer<WebSocketMessage> webSocketMessageConsumer) {
        this.simpleHttpServer.setWebSocketMessageConsumer(webSocketMessageConsumer);
    }

    @Override
    public void setWebSocketCloseConsumer(final Consumer<WebSocketMessage> webSocketMessageConsumer) {
        this.simpleHttpServer.setWebSocketCloseConsumer(webSocketMessageConsumer);
    }

    @Override
    public void setHttpRequestConsumer(final Consumer<HttpRequest> httpRequestConsumer) {
        this.simpleHttpServer.setHttpRequestConsumer(httpRequestConsumer);
    }

    @Override
    public void setHttpRequestsIdleConsumer(final Consumer<Void> idleRequestConsumer) {
        this.simpleHttpServer.setHttpRequestsIdleConsumer(
                aVoid -> {
                    idleRequestConsumer.accept(null);
                    vertxUtils.setTime(Timer.timer().now());
                }
        );
    }

    @Override
    public void setWebSocketIdleConsume(final Consumer<Void> idleWebSocketConsumer) {
        this.simpleHttpServer.setWebSocketIdleConsume(
                aVoid -> {
                    idleWebSocketConsumer.accept(null);
                    vertxUtils.setTime(Timer.timer().now());
                }
        );
    }

    @Override
    public void start() {
        startWithNotify(null);
    }

    @Override
    public void startWithNotify(final Runnable runnable) {

        simpleHttpServer.start();

        if (debug) {
            vertx.setPeriodic(10_000, event -> logger.info("Exception Count {} Close Count {}", exceptionCount, closeCount));
        }

        final io.vertx.core.http.HttpServerOptions vertxOptions = new io.vertx.core.http.HttpServerOptions();

        vertxOptions.setTcpNoDelay(options.isTcpNoDelay());
        vertxOptions.setSoLinger(options.getSoLinger());
        vertxOptions.setUsePooledBuffers(options.isUsePooledBuffers());
        vertxOptions.setReuseAddress(options.isReuseAddress());
        vertxOptions.setAcceptBacklog(options.getAcceptBackLog());
        vertxOptions.setTcpKeepAlive(options.isKeepAlive());
        vertxOptions.setCompressionSupported(options.isCompressionSupport());
        vertxOptions.setMaxWebsocketFrameSize(options.getMaxWebSocketFrameSize());
        vertxOptions.setSsl(options.isSsl());

        final JksOptions jksOptions = new JksOptions();
        jksOptions.setPath(options.getTrustStorePath());
        jksOptions.setPassword(options.getTrustStorePassword());

        vertxOptions.setTrustStoreOptions(jksOptions);
        httpServer = vertx.createHttpServer(vertxOptions);
        httpServer.websocketHandler(this::handleWebSocketMessage);
        httpServer.requestHandler(this::handleHttpRequest);

        if (Str.isEmpty(host)) {
            httpServer.listen(port, event -> {
                if (event.failed()) {
                    logger.error("HTTP SERVER unable to start on port " + port + " default host ");
                    simpleHttpServer.getErrorHandler().accept(event.cause());
                } else {

                    if (runnable != null) {
                        runnable.run();
                    }
                    logger.info("HTTP SERVER started on port " + port + " default host ");
                    simpleHttpServer.getOnStart().run();
                }
            });
        } else {
            httpServer.listen(port, host, event -> {
                if (event.failed()) {
                    logger.error("HTTP SERVER UNABLE to START on port " + port + " host " + host);
                    simpleHttpServer.getErrorHandler().accept(event.cause());
                } else {

                    if (runnable != null) runnable.run();
                    logger.info("HTTP SERVER started on port " + port + " host " + host);
                    simpleHttpServer.getOnStart().run();
                }
            });
        }

    }

    @Override
    public HttpServer startServerAndWait() {

        final CountDownLatch latch = new CountDownLatch(1);

        this.startWithNotify(() -> latch.countDown());

        try {
            latch.await(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.interrupted();
        }

        return this;
    }

    @Override
    public void stop() {
        simpleHttpServer.stop();
        try {
            if (httpServer != null) {

                httpServer.close();
            }

            if (startedVertx && vertx != null) {
                vertx.close();
            }
        } catch (Exception ex) {

            logger.info("HTTP SERVER unable to close " + port + " host " + host);
        }
        if (systemManager != null) systemManager.serviceShutDown();

    }

    /**
     * Handle a vertx request by converting it into a QBit request.
     *
     * @param request request
     */
    private void handleHttpRequest(final HttpServerRequest request) {

        if (debug) {
            setupMetrics(request);
            logger.debug("HttpServerVertx::handleHttpRequest::{}:{}", request.method(), request.uri());
        }

        switch (request.method().toString()) {

            case "PUT":
            case "POST":
            case "OPTIONS":
            case "TRACE":
            case "DELETE":
            case "CONNECT":
                handleRequestWithBody(request);
                break;

            case "HEAD":
            case "GET":
                handleRequestWithNoBody(request);
                break;

            default:
                throw new IllegalStateException("method not supported yet " + request.method());

        }

    }

    private void handleRequestWithNoBody(HttpServerRequest request) {
        final HttpRequest getRequest;
        getRequest = vertxUtils.createRequest(request, null, new HashMap<>(),
                simpleHttpServer.getDecorators(), simpleHttpServer.getHttpResponseCreator());
        simpleHttpServer.handleRequest(getRequest);
    }

    private void handleRequestWithBody(HttpServerRequest request) {
        final String contentType = request.headers().get("Content-Type");

        if (HttpContentTypes.isFormContentType(contentType)) {
            request.setExpectMultipart(true);
        }

        final Buffer[] bufferHolder = new Buffer[1];
        final HttpRequest bodyHttpRequest = vertxUtils.createRequest(request, () -> bufferHolder[0], new HashMap<>(),
                simpleHttpServer.getDecorators(), simpleHttpServer.getHttpResponseCreator());
        if (simpleHttpServer.getShouldContinueReadingRequestBody().test(bodyHttpRequest)) {
            request.bodyHandler((buffer) -> {
                bufferHolder[0] = buffer;
                simpleHttpServer.handleRequest(bodyHttpRequest);
            });
        } else {
            logger.info("Request body rejected {} {}", request.method(), request.absoluteURI());
        }
    }

    private void setupMetrics(final HttpServerRequest request) {

        request.exceptionHandler(event -> {

            if (debug) {
                exceptionCount++;
            }

            logger.info("EXCEPTION", event);

        });

        request.endHandler(event -> {

            if (debug) {
                closeCount++;
            }

            logger.info("REQUEST OVER");
        });
    }

    private void handleWebSocketMessage(final ServerWebSocket webSocket) {
        simpleHttpServer.handleOpenWebSocket(vertxUtils.createWebSocket(webSocket));
    }

    @Override
    public void setWebSocketOnOpenConsumer(Consumer<WebSocket> onOpenConsumer) {
        this.simpleHttpServer.setWebSocketOnOpenConsumer(onOpenConsumer);
    }

}


/**
 * ****************************************************************************
 * Copyright (c) 2015. Rick Hightower, Geoff Chandler
 *

 * 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.
 * __          __  _     _____            _        _
 * \ \        / / | |   / ____|          | |      | |
 * \ \  /\  / /__| |__| (___   ___   ___| | _____| |_
 * \ \/  \/ / _ \ '_ \\___ \ / _ \ / __| |/ / _ \ __|
 * \  /\  /  __/ |_) |___) | (_) | (__|   <  __/ |_
 * \/  \/ \___|_.__/_____/ \___/ \___|_|\_\___|\__|
 * _  _____  ____  _   _
 * | |/ ____|/ __ \| \ | |
 * | | (___ | |  | |  \| |
 * _   | |\___ \| |  | | . ` |
 * | |__| |____) | |__| | |\  |
 * \____/|_____/ \____/|_|_\_|_
 * |  __ \|  ____|/ ____|__   __|
 * | |__) | |__  | (___    | |
 * |  _  /|  __|  \___ \   | |
 * | | \ \| |____ ____) |  | |
 * |_|  \_\______|_____/   |_|___                 _
 * |  \/  (_)              / ____|               (_)
 * | \  / |_  ___ _ __ ___| (___   ___ _ ____   ___  ___ ___
 * | |\/| | |/ __| '__/ _ \\___ \ / _ \ '__\ \ / / |/ __/ _ \
 * | |  | | | (__| | | (_) |___) |  __/ |   \ V /| | (_|  __/
 * |_|  |_|_|\___|_|  \___/_____/ \___|_|    \_/ |_|\___\___|
 *

 * QBit - The Microservice lib for Java : JSON, WebSocket, REST. Be The Web!
 * http://rick-hightower.blogspot.com/2014/12/rise-of-machines-writing-high-speed.html
 * http://rick-hightower.blogspot.com/2014/12/quick-guide-to-programming-services-in.html
 * http://rick-hightower.blogspot.com/2015/01/quick-startClient-qbit-programming.html
 * http://rick-hightower.blogspot.com/2015/01/high-speed-soa.html
 * http://rick-hightower.blogspot.com/2015/02/qbit-event-bus.html
 * ****************************************************************************
 */

package io.advantageous.qbit.service.impl;

import io.advantageous.boon.core.reflection.BeanUtils;
import io.advantageous.boon.core.reflection.ClassMeta;
import io.advantageous.qbit.Factory;
import io.advantageous.qbit.GlobalConstants;
import io.advantageous.qbit.boon.service.impl.BoonInvocationHandlerForSendQueue;
import io.advantageous.qbit.client.BeforeMethodSent;
import io.advantageous.qbit.client.ClientProxy;
import io.advantageous.qbit.concurrent.PeriodicScheduler;
import io.advantageous.qbit.events.EventManager;
import io.advantageous.qbit.message.*;
import io.advantageous.qbit.queue.*;
import io.advantageous.qbit.service.*;
import io.advantageous.qbit.system.QBitSystemManager;
import io.advantageous.qbit.time.Duration;
import io.advantageous.qbit.transforms.NoOpResponseTransformer;
import io.advantageous.qbit.transforms.Transformer;
import io.advantageous.qbit.util.Timer;
import io.advantageous.reakt.promise.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import static io.advantageous.qbit.QBit.factory;
import static io.advantageous.qbit.service.ServiceContext.serviceContext;

/**
 * @author rhightower on 2/18/15.
 */
public class BaseServiceQueueImpl implements ServiceQueue {
    protected static final ThreadLocal<ServiceQueue> serviceThreadLocal = new ThreadLocal<>();
    protected final QBitSystemManager systemManager;
    protected final Logger logger = LoggerFactory.getLogger(ServiceQueueImpl.class);
    protected final boolean debug = GlobalConstants.DEBUG && logger.isDebugEnabled();
    protected final Object service;
    protected final Queue<Response<Object>> responseQueue;
    protected final Queue<MethodCall<Object>> requestQueue;
    protected final Queue<Event<Object>> eventQueue;
    protected final QueueBuilder requestQueueBuilder;
    protected final QueueBuilder responseQueueBuilder;
    protected final boolean handleCallbacks;
    protected final ServiceMethodHandler serviceMethodHandler;
    protected final SendQueue<Response<Object>> responseSendQueue;
    private final Factory factory;
    private final BeforeMethodSent beforeMethodSent;
    private final Optional<EventManager> eventManager;
    private final boolean joinEventManager;
    private final AtomicBoolean started = new AtomicBoolean(false);
    private final BeforeMethodCall beforeMethodCall;
    private final BeforeMethodCall beforeMethodCallAfterTransform;
    private final AfterMethodCall afterMethodCall;
    private final AfterMethodCall afterMethodCallAfterTransform;
    private final CallbackManager callbackManager;
    private final QueueCallBackHandler queueCallBackHandler;
    protected volatile long lastResponseFlushTime = Timer.timer().now();
    private Transformer<Request, Object> requestObjectTransformer = ServiceConstants.NO_OP_ARG_TRANSFORM;
    private Transformer<Response<Object>, Response> responseObjectTransformer = new NoOpResponseTransformer();
    private AtomicBoolean failing = new AtomicBoolean();

    public BaseServiceQueueImpl(final String rootAddress,
                                final String serviceAddress,
                                final Object service,
                                final QueueBuilder requestQueueBuilder,
                                final QueueBuilder responseQueueBuilder,
                                final ServiceMethodHandler serviceMethodHandler,
                                final Queue<Response<Object>> responseQueue,
                                final boolean async,
                                final boolean handleCallbacks,
                                final QBitSystemManager systemManager,
                                final BeforeMethodCall beforeMethodCall,
                                final BeforeMethodCall beforeMethodCallAfterTransform,
                                final AfterMethodCall afterMethodCall,
                                final AfterMethodCall afterMethodCallAfterTransform,
                                final QueueCallBackHandler queueCallBackHandler,
                                final CallbackManager callbackManager,
                                final BeforeMethodSent beforeMethodSent,
                                final EventManager eventManager,
                                final boolean joinEventManager) {

        this.eventManager = Optional.ofNullable(eventManager);

        this.joinEventManager = joinEventManager;

        this.beforeMethodSent = beforeMethodSent;
        this.beforeMethodCall = beforeMethodCall;
        this.beforeMethodCallAfterTransform = beforeMethodCallAfterTransform;
        this.afterMethodCall = afterMethodCall;
        this.afterMethodCallAfterTransform = afterMethodCallAfterTransform;

        this.callbackManager = callbackManager;

        if (queueCallBackHandler == null) {
            this.queueCallBackHandler = new QueueCallBackHandler() {
                @Override
                public void queueLimit() {

                }

                @Override
                public void queueEmpty() {

                }
            };
        } else {
            this.queueCallBackHandler = queueCallBackHandler;
        }
        if (requestQueueBuilder == null) {
            this.requestQueueBuilder = new QueueBuilder();
        } else {
            this.requestQueueBuilder = BeanUtils.copy(requestQueueBuilder);
        }

        if (responseQueueBuilder == null) {
            this.responseQueueBuilder = new QueueBuilder();
        } else {
            this.responseQueueBuilder = BeanUtils.copy(responseQueueBuilder);
        }

        if (responseQueue == null) {

            logger.info("RESPONSE QUEUE WAS NULL CREATING ONE for service");
            this.responseQueue = this.responseQueueBuilder.setName("Response Queue  " + serviceMethodHandler.address()).build();
        } else {
            this.responseQueue = responseQueue;
        }

        this.responseSendQueue = this.responseQueue.sendQueueWithAutoFlush(100, TimeUnit.MILLISECONDS);
        this.service = service;
        this.serviceMethodHandler = serviceMethodHandler;
        this.serviceMethodHandler.init(service, rootAddress, serviceAddress, responseSendQueue);
        this.eventQueue = this.requestQueueBuilder.setName("Event Queue" + serviceMethodHandler.address()).build();
        this.handleCallbacks = handleCallbacks;
        this.requestQueue = initRequestQueue(serviceMethodHandler, async);
        this.systemManager = systemManager;

        this.factory = factory();

        this.eventManager.ifPresent(em ->
        {

            em.joinService(BaseServiceQueueImpl.this);
        });

    }

    public static ServiceQueue currentService() {
        return serviceThreadLocal.get();
    }

    /**
     * This method is where all of the action is.
     *
     * @param methodCall           methodCall
     * @param serviceMethodHandler handler
     */
    private boolean doHandleMethodCall(MethodCall<Object> methodCall,
                                       final ServiceMethodHandler serviceMethodHandler) {
        if (debug) {
            logger.debug("ServiceImpl::doHandleMethodCall() METHOD CALL" + methodCall);
        }
        if (callbackManager != null) {

            if (methodCall.hasCallback() && serviceMethodHandler.couldHaveCallback(methodCall.name())) {
                callbackManager.registerCallbacks(methodCall);
            }
        }
        //inputQueueListener.receive(methodCall);
        final boolean continueFlag[] = new boolean[1];
        methodCall = beforeMethodProcessing(methodCall, continueFlag);
        if (continueFlag[0]) {
            if (debug) logger.debug("ServiceImpl::doHandleMethodCall() before handling stopped processing");
            return false;
        }
        Response<Object> response = serviceMethodHandler.receiveMethodCall(methodCall);
        if (response != ServiceConstants.VOID) {

            if (!afterMethodCall.after(methodCall, response)) {
                return false;
            }
            //noinspection unchecked
            response = responseObjectTransformer.transform(response);

            if (!afterMethodCallAfterTransform.after(methodCall, response)) {
                return false;
            }

            if (debug) {
                if (response.body() instanceof Throwable) {

                    logger.error("Unable to handle call ", ((Throwable) response.body()));

                }
            }
            if (!responseSendQueue.send(response)) {
                logger.error("Unable to send response {} for method {} for object {}",
                        response,
                        methodCall.name(),
                        methodCall.objectName());
            }

        }

        return false;
    }

    @Override
    public void start() {

        start(serviceMethodHandler, joinEventManager);

    }

    public ServiceQueue startServiceQueue() {

        start(serviceMethodHandler, joinEventManager);
        return this;
    }

    public ServiceQueue start(boolean joinEventManager) {
        start(serviceMethodHandler, joinEventManager);
        return this;
    }

    @Override
    public Queue<MethodCall<Object>> requestQueue() {
        return this.requestQueue;
    }

    @Override
    public Queue<Response<Object>> responseQueue() {
        return this.responseQueue;
    }

    protected Queue<MethodCall<Object>> initRequestQueue(final ServiceMethodHandler serviceMethodHandler, boolean async) {
        Queue<MethodCall<Object>> requestQueue;
        if (async) {
            requestQueue = this.requestQueueBuilder.setName("Send Queue  " + serviceMethodHandler.address()).build();
        } else {
            requestQueue = new Queue<MethodCall<Object>>() {
                @Override
                public ReceiveQueue<MethodCall<Object>> receiveQueue() {

                    return null;
                }

                @Override
                public SendQueue<MethodCall<Object>> sendQueue() {
                    return new SendQueue<MethodCall<Object>>() {
                        @Override
                        public boolean send(MethodCall<Object> item) {

                            return doHandleMethodCall(item, serviceMethodHandler);
                        }

                        @Override
                        public void sendAndFlush(MethodCall<Object> item) {

                            doHandleMethodCall(item, serviceMethodHandler);
                        }

                        @SafeVarargs
                        @Override
                        public final void sendMany(MethodCall<Object>... items) {

                            for (MethodCall<Object> item : items) {

                                doHandleMethodCall(item, serviceMethodHandler);
                            }
                        }

                        @Override
                        public void sendBatch(Collection<MethodCall<Object>> items) {

                            for (MethodCall<Object> item : items) {

                                doHandleMethodCall(item, serviceMethodHandler);
                            }

                        }

                        @Override
                        public void sendBatch(Iterable<MethodCall<Object>> items) {

                            for (MethodCall<Object> item : items) {

                                doHandleMethodCall(item, serviceMethodHandler);
                            }

                        }

                        @Override
                        public boolean shouldBatch() {
                            return false;
                        }

                        @Override
                        public void flushSends() {

                        }

                        @Override
                        public int size() {
                            return 0;
                        }
                    };
                }

                @Override
                public void startListener(ReceiveQueueListener<MethodCall<Object>> listener) {

                }

                @Override
                public void stop() {

                }

                @Override
                public int size() {
                    return 0;
                }
            };
        }
        return requestQueue;
    }

    public ServiceQueue startCallBackHandler() {
        if (!handleCallbacks) {
            /** Need to make this configurable. */
            callbackManager.startReturnHandlerProcessor(this.responseQueue);
            return this;
        } else {
            throw new IllegalStateException("Unable to handle callbacks in a new thread when handleCallbacks is set");
        }
    }

    public BaseServiceQueueImpl requestObjectTransformer(Transformer<Request, Object> requestObjectTransformer) {
        this.requestObjectTransformer = requestObjectTransformer;
        return this;
    }

    public BaseServiceQueueImpl responseObjectTransformer(Transformer<Response<Object>, Response> responseObjectTransformer) {
        this.responseObjectTransformer = responseObjectTransformer;
        return this;
    }

    private void start(final ServiceMethodHandler serviceMethodHandler,
                       final boolean joinEventManager) {

        if (started.get()) {
            logger.warn("Service {} already started. It will not start twice.", name());
            return;
        }

        logger.info("Starting service {}", name());
        started.set(true);

        final ReceiveQueue<Response<Object>> responseReceiveQueue =
                this.handleCallbacks ?
                        responseQueue.receiveQueue() : null;

        final ReceiveQueue<Event<Object>> eventReceiveQueue =
                eventQueue.receiveQueue();

        serviceThreadLocal.set(this);

        if (!(service instanceof EventManager)) {
            if (joinEventManager) {
                serviceContext().eventManager().joinService(this);
            }
        }
        flushEventManagerCalls();
        serviceThreadLocal.set(null);
        requestQueue.startListener(new ReceiveQueueListener<MethodCall<Object>>() {

            @Override
            public void init() {

                serviceThreadLocal.set(BaseServiceQueueImpl.this);
                queueCallBackHandler.queueInit();
                serviceMethodHandler.init();
            }

            @Override
            public void receive(MethodCall<Object> methodCall) {
                queueCallBackHandler.beforeReceiveCalled();
                doHandleMethodCall(methodCall, serviceMethodHandler);
                queueCallBackHandler.afterReceiveCalled();
            }

            @Override
            public void empty() {

                serviceThreadLocal.set(BaseServiceQueueImpl.this);
                handle();
                serviceMethodHandler.empty();
                queueCallBackHandler.queueEmpty();
            }

            @Override
            public void startBatch() {
                serviceThreadLocal.set(BaseServiceQueueImpl.this);
                serviceMethodHandler.startBatch();
                queueCallBackHandler.queueStartBatch();
            }

            @Override
            public void limit() {

                serviceThreadLocal.set(BaseServiceQueueImpl.this);
                handle();
                serviceMethodHandler.limit();
                queueCallBackHandler.queueLimit();
            }

            @Override
            public void shutdown() {

                serviceThreadLocal.set(BaseServiceQueueImpl.this);
                handle();
                serviceMethodHandler.shutdown();
                queueCallBackHandler.queueShutdown();

                serviceThreadLocal.set(null);
            }

            @Override
            public void idle() {

                serviceThreadLocal.set(BaseServiceQueueImpl.this);
                handle();
                serviceMethodHandler.idle();
                queueCallBackHandler.queueIdle();
                if (callbackManager != null) {
                    callbackManager.process(0);
                }
                serviceThreadLocal.set(null);
            }

            /** Such a small method with so much responsibility. */
            public void handle() {
                manageResponseQueue();
                handleCallBacks(responseReceiveQueue);
                handleEvents(eventReceiveQueue, serviceMethodHandler);
            }

        });
    }

    private void handleEvents(ReceiveQueue<Event<Object>> eventReceiveQueue, ServiceMethodHandler serviceMethodHandler) {
    /* Handles the event processing. */
        Event<Object> event = eventReceiveQueue.poll();
        while (event != null) {
            serviceMethodHandler.handleEvent(event);
            event = eventReceiveQueue.poll();
        }
        flushEventManagerCalls();
    }

    private void handleCallBacks(ReceiveQueue<Response<Object>> responseReceiveQueue) {
    /* Handles the CallBacks if you have configured the service
    to handle its own callbacks.
    Callbacks can be handled in a separate thread or the same
    thread the manages the service.
     */
        if (handleCallbacks) {
            Response<Object> response = responseReceiveQueue.poll();
            while (response != null) {
                callbackManager.handleResponse(response);
                response = responseReceiveQueue.poll();
            }
        }
    }

    private void flushEventManagerCalls() {
        final EventManager eventManager = factory.eventManagerProxy();
        if (eventManager != null) {
            ServiceProxyUtils.flushServiceProxy(eventManager);
            factory.clearEventManagerProxy();
        }
    }

    private void manageResponseQueue() {
        long now = Timer.timer().now();
        if (now - lastResponseFlushTime > 50) {
            lastResponseFlushTime = now;
            responseSendQueue.flushSends();
        }
    }

    private MethodCall<Object> beforeMethodProcessing(MethodCall<Object> methodCall, boolean[] continueFlag) {
        if (!beforeMethodCall.before(methodCall)) {
            continueFlag[0] = false;
        }
        if (requestObjectTransformer != null && requestObjectTransformer != ServiceConstants.NO_OP_ARG_TRANSFORM) {
            final Object arg = requestObjectTransformer.transform(methodCall);
            methodCall = MethodCallBuilder.transformed(methodCall, arg);
        }
        if (beforeMethodCallAfterTransform != null && beforeMethodCallAfterTransform != ServiceConstants.NO_OP_BEFORE_METHOD_CALL) {
            if (!beforeMethodCallAfterTransform.before(methodCall)) {
                continueFlag[0] = false;
            }
        }
        return methodCall;
    }

    @Override
    public SendQueue<MethodCall<Object>> requests() {
        return requestQueue.sendQueue();
    }

    @Override
    public SendQueue<MethodCall<Object>> requestsWithAutoFlush(int flushInterval, TimeUnit timeUnit) {
        return requestQueue.sendQueueWithAutoFlush(flushInterval, timeUnit);
    }

    @Override
    public ReceiveQueue<Response<Object>> responses() {
        return responseQueue.receiveQueue();
    }

    @Override
    public String name() {
        return serviceMethodHandler.name();
    }

    @Override
    public String address() {
        return serviceMethodHandler.address();
    }

    @Override
    public void stop() {

        started.set(false);
        try {
            if (requestQueue != null) requestQueue.stop();
        } catch (Exception ex) {
            if (debug) logger.debug("Unable to stop request queue", ex);
        }

        try {
            if (responseQueue != null) responseQueue.stop();
        } catch (Exception ex) {
            if (debug) logger.debug("Unable to stop response queues", ex);
        }

        if (systemManager != null) {
            this.systemManager.serviceShutDown();
            this.systemManager.unregisterService(this);
        }

        if (!(service instanceof EventManager)) {
            if (joinEventManager) {
                serviceContext().eventManager().leaveEventBus(this);
            }
        }

        eventManager.ifPresent(em -> em.leaveEventBus(BaseServiceQueueImpl.this));
    }

    @Override
    public Collection<String> addresses(String address) {
        return this.serviceMethodHandler.addresses();
    }

    @Override
    public void flush() {
        lastResponseFlushTime = 0;
        manageResponseQueue();
    }

    @Override
    public boolean failing() {
        return failing.get();
    }

    @Override
    public boolean running() {
        return started.get();
    }

    @Override
    public void setFailing() {
        failing.set(true);
    }

    @Override
    public void recover() {
        failing.set(false);
    }

    public Object service() {
        return service;
    }

    public <T> T createProxyWithAutoFlush(Class<T> serviceInterface, int interval, TimeUnit timeUnit) {

        final SendQueue<MethodCall<Object>> methodCallSendQueue = requestQueue.sendQueueWithAutoFlush(interval, timeUnit);
        methodCallSendQueue.start();
        return proxy(serviceInterface, methodCallSendQueue);

    }

    @Override
    public <T> T createProxyWithAutoFlush(Class<T> serviceInterface, Duration duration) {
        return createProxyWithAutoFlush(serviceInterface, (int) duration.getDuration(), duration.getTimeUnit());
    }

    public <T> T createProxyWithAutoFlush(final Class<T> serviceInterface,
                                          final PeriodicScheduler periodicScheduler,
                                          final int interval, final TimeUnit timeUnit) {

        final SendQueue<MethodCall<Object>> methodCallSendQueue =
                requestQueue.sendQueueWithAutoFlush(periodicScheduler, interval, timeUnit);
        methodCallSendQueue.start();
        return proxy(serviceInterface, methodCallSendQueue);

    }

    public <T> T createProxy(Class<T> serviceInterface) {
        final SendQueue<MethodCall<Object>> methodCallSendQueue = requestQueue.sendQueue();
        return proxy(serviceInterface, methodCallSendQueue);
    }

    private <T> void validateInterface(Class<T> serviceInterface) {
        if (!serviceInterface.isInterface()) {
            throw new IllegalStateException("Service Interface must be an interface " + serviceInterface.getName());
        }
        final ClassMeta<T> classMeta = ClassMeta.classMeta(serviceInterface);

        final Method[] declaredMethods = classMeta.cls().getDeclaredMethods();

        for (Method m : declaredMethods) {
            if (!(m.getReturnType() == void.class || Promise.class.isAssignableFrom(m.getReturnType()))) {
                throw new IllegalStateException("Async interface can only return void or a Promise " + serviceInterface.getName());
            }
        }

    }

    private <T> T proxy(final Class<T> serviceInterface,
                        final SendQueue<MethodCall<Object>> methodCallSendQueue) {

        validateInterface(serviceInterface);

        if (!started.get()) {
            logger.debug("ServiceQueue::create(...), A proxy is being asked for a service that is not started ", name());
        }
        final InvocationHandler invocationHandler = new BoonInvocationHandlerForSendQueue(methodCallSendQueue,
                serviceInterface, serviceInterface.getSimpleName(), beforeMethodSent);
        final Object o = Proxy.newProxyInstance(serviceInterface.getClassLoader(),
                new Class[]{serviceInterface, ClientProxy.class}, invocationHandler
        );
        //noinspection unchecked
        return (T) o;
    }

    @Override
    public SendQueue<Event<Object>> events() {
        return this.eventQueue.sendQueueWithAutoFlush(50, TimeUnit.MILLISECONDS);
    }

    @Override
    public String toString() {
        return "ServiceQueue{" +
                "service=" + service.getClass().getSimpleName() +
                '}';
    }

}