package io.advantageous.qbit.admin;

import io.advantageous.boon.core.Sets;
import io.advantageous.boon.core.Str;
import io.advantageous.boon.core.Sys;
import io.advantageous.consul.discovery.ConsulServiceDiscoveryBuilder;
import io.advantageous.qbit.Factory;
import io.advantageous.qbit.QBit;
import io.advantageous.qbit.annotation.AnnotationUtils;
import io.advantageous.qbit.client.BeforeMethodSent;
import io.advantageous.qbit.client.BeforeMethodSentChain;
import io.advantageous.qbit.events.EventManager;
import io.advantageous.qbit.http.interceptor.ForwardCallMethodInterceptor;
import io.advantageous.qbit.http.server.HttpServerBuilder;
import io.advantageous.qbit.logging.SetupMdcForHttpRequestInterceptor;
import io.advantageous.qbit.meta.builder.ContextMetaBuilder;
import io.advantageous.qbit.metrics.support.LocalStatsCollectorBuilder;
import io.advantageous.qbit.metrics.support.StatServiceBuilder;
import io.advantageous.qbit.metrics.support.StatsDReplicatorBuilder;
import io.advantageous.qbit.queue.QueueCallBackHandler;
import io.advantageous.qbit.server.EndpointServerBuilder;
import io.advantageous.qbit.server.ServiceEndpointServer;
import io.advantageous.qbit.service.*;
import io.advantageous.qbit.service.discovery.ServiceDiscovery;
import io.advantageous.qbit.service.discovery.dns.DnsUtil;
import io.advantageous.qbit.service.discovery.lokate.LokateServiceDiscoveryProvider;
import io.advantageous.qbit.service.health.HealthServiceAsync;
import io.advantageous.qbit.service.health.HealthServiceBuilder;
import io.advantageous.qbit.service.health.ServiceHealthManager;
import io.advantageous.qbit.service.impl.ServiceHealthManagerDefault;
import io.advantageous.qbit.service.stats.StatsCollector;
import io.advantageous.qbit.system.QBitSystemManager;

import java.net.URI;
import java.util.*;
import java.util.function.Supplier;

import static io.advantageous.consul.discovery.ConsulServiceDiscoveryBuilder.consulServiceDiscoveryBuilder;

/**
 * This is a utility class for when you are running in a PaaS like Heroku or Docker.
 * It also allows you to share stat, health and system manager setup.
 *

 * The main end point port will be read from the environment variable {@code WEB_PORT}
 * and if not found then read from {@code PORT0}, and then lastly from {@code PORT}.
 *

 * If statsD is enabled then this will look for the statsd port and host from {@code STATSD_PORT} and
 * {@code STATSD_HOST} environment
 * variables.
 *

 * If you use the admin builder, the port for the admin will be from the environment variable
 * {@code QBIT_ADMIN_PORT}, then {@code ADMIN_PORT}, and then {@code PORT1}.
 * It can be overridden from system properties as well see AdminBuilder for more details.
 *

 * Defaults for ports and hosts can be overridden by their respective builders.
 **/
@SuppressWarnings("unused")
public class ManagedServiceBuilder {

    /**
     * Holds the system manager used to register proper shutdown with JVM.
     */
    private QBitSystemManager systemManager;

    /**
     * Holds the stats service builder used to collect system stats.
     */
    private StatServiceBuilder statServiceBuilder;

    /**
     * EndpointServerBuilder used to hold the main EndpointServerBuilder.
     */
    private EndpointServerBuilder endpointServerBuilder;

    /**
     * Used to create LocalStatsCollection.
     */
    private LocalStatsCollectorBuilder localStatsCollectorBuilder;

    /**
     * Used to create the main http server.
     */
    private HttpServerBuilder httpServerBuilder;

    /**
     * Enables local stats collection.
     */
    private boolean enableLocalStats = true;

    /**
     * Enables sending stats to stats D.
     */
    private boolean enableStatsD = false;

    /**
     * Enables local health stats collection.
     */
    private boolean enableLocalHealth = true;

    /**
     * Used to create local health collector.
     */
    private HealthServiceBuilder healthServiceBuilder;

    /**
     * Health service used to collect health info from service queues.
     */
    private HealthServiceAsync healthService;

    /**
     * Enables the collection of stats.
     */
    private boolean enableStats = true;

    /**
     * Event manager for services, service queues, and end point servers.
     */
    private EventManager eventManager;

    /**
     * Factory to create QBit parts.
     */
    private Factory factory;

    /**
     * Builder to hold context information about endpoints.
     */
    private ContextMetaBuilder contextMetaBuilder;

    /**
     * Endpoint services that will be exposed through contextMetaBuilder.
     */
    private List<Object> endpointServices;

    /**
     * Endpoint services that will be exposed through contextMetaBuilder.
     */
    private Map<String, Object> endpointServiceMapWithAlias;

    /**
     * Endpoint services that will be exposed through contextMetaBuilder.
     */
    private List<ManagedServiceDefinition> managedServiceDefinitionList;

    /**
     * The builder for the admin.
     */
    private AdminBuilder adminBuilder;

    /**
     * StatsD replicator builder.
     */
    private StatsDReplicatorBuilder statsDReplicatorBuilder;

    /**
     * Service Discovery.
     */
    private ServiceDiscovery serviceDiscovery;

    /**
     * Service Discovery supplier.
     */
    private Supplier<ServiceDiscovery> serviceDiscoverySupplier;

    /**
     * Default Root URI.
     */
    private String rootURI = Sys.sysProp(ManagedServiceBuilder.class.getName()
            + ".rootURI", "/services");

    /**
     * Default public host used for swagger.
     */
    private String publicHost = Sys.sysProp(ManagedServiceBuilder.class.getName()
            + ".publicHost", "localhost");

    /**
     * Actual port.
     */
    private int port = Sys.sysProp(ManagedServiceBuilder.class.getName()
            + ".port", EndpointServerBuilder.DEFAULT_PORT);

    /**
     * Public port used for swagger.
     */
    private int publicPort = Sys.sysProp(ManagedServiceBuilder.class.getName()
            + ".publicPort", -1);

    /**
     * How often stats should be flushed.
     */
    private int sampleStatFlushRate = Sys.sysProp(ManagedServiceBuilder.class.getName()
            + ".sampleStatFlushRate", 5);

    /**
     * How often timings should be collected defaults to every 100 calls.
     */
    private int checkTimingEveryXCalls = Sys.sysProp(ManagedServiceBuilder.class.getName()
            + ".checkTimingEveryXCalls", 100);

    /**
     * Turn on Logging Mapped Diagnostic Context.
     */
    private boolean enableLoggingMappedDiagnosticContext = Sys.sysProp(ManagedServiceBuilder.class.getName()
            + ".enableLoggingMappedDiagnosticContext", false);

    /**
     * Turn on Request chain construction so original request is sent to other downstream internal services.
     */
    private boolean enableRequestChain = Sys.sysProp(ManagedServiceBuilder.class.getName()
            + ".enableRequestChain", false);

    /**
     * Used to track headers for MDC.
     */
    private Set<String> requestHeadersToTrackForMappedDiagnosticContext;

    /**
     * Create ManagedServiceBuilder which looks for MicroserviceConfig file.
     *
     * @param serviceName service name
     */
    public ManagedServiceBuilder(final String serviceName) {
        if (serviceName != null) {

            final MicroserviceConfig config = MicroserviceConfig.readConfig(serviceName);

            /** Configure ManagedServiceBuilder. */
            this.setRootURI(config.getRootURI());
            this.setPublicHost(config.getPublicHost());
            this.setPublicPort(config.getPublicPort());
            this.setPort(config.getPort());

            /** Configure ManagedServiceBuilder. */
            final ContextMetaBuilder contextMetaBuilder = this.getContextMetaBuilder();
            contextMetaBuilder.setLicenseName(config.getLicenseName());
            contextMetaBuilder.setLicenseURL(config.getLicenseURL());
            contextMetaBuilder.setContactURL(config.getContactURL());
            contextMetaBuilder.setTitle(config.getTitle());
            contextMetaBuilder.setVersion(config.getVersion());
            contextMetaBuilder.setContactName(config.getContactName());
            contextMetaBuilder.setContactEmail(config.getContactEmail());
            contextMetaBuilder.setDescription(config.getDescription());

            /** Configure statsD. */
            if (config.isStatsD()) {
                this.setEnableStatsD(true);
                this.getStatsDReplicatorBuilder().setHost(config.getStatsDHost());

                if (config.getStatsDPort() != -1) {
                    this.getStatsDReplicatorBuilder().setPort(config.getStatsDPort());
                }
            }

            this.setCheckTimingEveryXCalls(config.getCheckTimingEveryXCalls());
            this.setSampleStatFlushRate(config.getSampleStatFlushRate());

            this.setEnableLocalStats(config.isEnableLocalStats());
            this.setEnableStats(config.isEnableStats());
            this.setEnableLocalHealth(config.isEnableLocalHealth());
        }
    }

    public static ManagedServiceBuilder managedServiceBuilder() {
        return new ManagedServiceBuilder(null);
    }

    public static ManagedServiceBuilder managedServiceBuilder(final String serviceName) {
        return new ManagedServiceBuilder(serviceName);
    }

    private List<ManagedServiceDefinition> getManagedServiceDefinitionList() {
        if (managedServiceDefinitionList == null) {
            managedServiceDefinitionList = new ArrayList<>();
        }
        return managedServiceDefinitionList;
    }

    /**
     * Enable the logging diagnostic context
     *
     * @return this
     */
    public ManagedServiceBuilder enableLoggingMappedDiagnosticContext() {
        this.enableRequestChain = true;
        this.enableLoggingMappedDiagnosticContext = true;
        return this;
    }

    /**
     * Enable the logging diagnostic context
     *
     * @param requestHeaders request headers
     * @return this, fluent
     */
    public ManagedServiceBuilder enableLoggingMappedDiagnosticContext(final String... requestHeaders) {
        return enableLoggingMappedDiagnosticContext(Sets.set(requestHeaders));
    }

    /**
     * Enable the logging diagnostic context
     *
     * @param requestHeaders request headers to track in the MDC.
     * @return this
     */
    public ManagedServiceBuilder enableLoggingMappedDiagnosticContext(final Set<String> requestHeaders) {
        this.enableRequestChain = true;
        this.enableLoggingMappedDiagnosticContext = true;
        this.requestHeadersToTrackForMappedDiagnosticContext = Collections.unmodifiableSet(requestHeaders);
        return this;
    }

    /**
     * Enable the request chain.  There is overhead for this, but this allows REST and WebSocket
     * services to pass the originating request, methodCall, etc. to downstream services.
     * Where it will be available via the RequestContext.
     * A MethodCall, HttpRequest, WebSocketMessage are all Requests in QBit.
     *
     * @return this
     */
    public ManagedServiceBuilder enableRequestChain() {
        this.enableRequestChain = true;
        return this;
    }

    /**
     * @return logging MDC enabled or not
     */
    public boolean isEnableLoggingMappedDiagnosticContext() {
        return enableLoggingMappedDiagnosticContext;
    }

    /**
     * Enable or disable logging MDC.
     *
     * @param enableLoggingMappedDiagnosticContext enableLoggingMappedDiagnosticContext
     * @return this
     */
    public ManagedServiceBuilder setEnableLoggingMappedDiagnosticContext(
            final boolean enableLoggingMappedDiagnosticContext) {
        this.enableLoggingMappedDiagnosticContext = enableLoggingMappedDiagnosticContext;
        return this;
    }

    /**
     * Request call chain building enabled. This allows REST and WebSocket
     * services to pass the originating request, methodCall, etc. to downstream services.
     * Where it will be available via the RequestContext.
     * A MethodCall, HttpRequest, WebSocketMessage are all Requests in QBit.
     *
     * @return is call chain tracking enabled.
     */
    public boolean isEnableRequestChain() {
        return enableRequestChain;
    }

    /**
     * Enable or disable request chain.  There is overhead for this, but this allows REST and WebSocket
     * services to pass the originating request, methodCall, etc. to downstream services.
     * Where it will be available via the RequestContext.
     * A MethodCall, HttpRequest, WebSocketMessage are all Requests in QBit.
     *
     * @param enableRequestChain enableRequestChain
     * @return this
     */
    public ManagedServiceBuilder setEnableRequestChain(final boolean enableRequestChain) {
        this.enableRequestChain = enableRequestChain;
        return this;
    }

    /**
     * Get the public host for service meta generation (Swagger)
     *
     * @return public host
     */
    public String getPublicHost() {

        /** This works in a Heroku like environment. */
        if (System.getenv("PUBLIC_HOST") != null) {
            publicHost = System.getenv("PUBLIC_HOST");
        }

        /** This works in a mesosphere like environment. */
        if (System.getenv("HOST") != null) {
            publicHost = System.getenv("HOST");
        }

        return publicHost;
    }

    /**
     * Set the public host for service meta generation (Swagger)
     *
     * @param publicHost publicHost
     * @return this
     */
    public ManagedServiceBuilder setPublicHost(String publicHost) {
        this.publicHost = publicHost;
        return this;
    }

    /**
     * Creates a ServiceHealthManager.
     *
     * @return ServiceHealthManager
     */
    public ServiceHealthManager createServiceHealthManager() {
        return new ServiceHealthManagerDefault(null, null);
    }

    /**
     * Creates a ServiceHealthManager.
     *
     * @param failCallback called if the service reports a failure.
     * @return ServiceHealthManager
     */
    public ServiceHealthManager createServiceHealthManager(final Runnable failCallback) {
        return new ServiceHealthManagerDefault(failCallback, null);
    }

    /**
     * Creates a ServiceHealthManager.
     *
     * @param failCallback    called if the service reports a failure.
     * @param recoverCallback recoverCallback called if the service recovers from a failure.
     * @return ServiceHealthManager
     */
    public ServiceHealthManager createServiceHealthManager(final Runnable failCallback,
                                                           final Runnable recoverCallback) {
        return new ServiceHealthManagerDefault(failCallback, recoverCallback);
    }

    /**
     * Get the public port for service meta generation (Swagger)
     *
     * @return public port
     */
    public int getPublicPort() {
        if (publicPort == -1) {

            String sport = System.getenv("PUBLIC_WEB_PORT");

            if (!Str.isEmpty(sport)) {
                publicPort = Integer.parseInt(sport);
            }
        }

        if (publicPort == -1) {
            publicPort = getPort();
        }

        return publicPort;
    }

    /**
     * Set the public port for service meta generation (Swagger)
     *
     * @param publicPort publicPort
     * @return this
     */
    public ManagedServiceBuilder setPublicPort(int publicPort) {
        this.publicPort = publicPort;
        return this;
    }

    /**
     * Get the actual port to bind to.
     *

     * Defaults to EndpointServerBuilder.DEFAULT_PORT.
     *

     * Looks for PORT under PORT_WEB, PORT0, PORT.
     *
     * @return actual http port to bind to.
     */
    public int getPort() {

        if (port == EndpointServerBuilder.DEFAULT_PORT) {
            String sport = System.getenv("PORT_WEB");
            /** Looks up port for Mesoshpere and the like. */
            if (Str.isEmpty(sport)) {
                sport = System.getenv("PORT0");
            }
            if (Str.isEmpty(sport)) {
                sport = System.getenv("PORT");
            }
            if (!Str.isEmpty(sport)) {
                port = Integer.parseInt(sport);
            }
        }
        return port;
    }

    /**
     * Set the actual port to bind to.
     *
     * @param port port
     * @return this
     */
    public ManagedServiceBuilder setPort(int port) {
        this.port = port;
        return this;
    }

    /**
     * Get the root URI
     *
     * @return root URI.
     */
    public String getRootURI() {
        return rootURI;
    }

    /**
     * Set rootURI root uri
     *
     * @param rootURI rootURI
     * @return this, fluent
     */
    public ManagedServiceBuilder setRootURI(String rootURI) {
        this.rootURI = rootURI;
        return this;
    }

    /**
     * Enable consul discovery
     *
     * @param dataCenter name of datacenter we are connecting to.
     * @return fluent, this
     */
    public ManagedServiceBuilder enableConsulServiceDiscovery(final String dataCenter) {
        final ConsulServiceDiscoveryBuilder consulServiceDiscoveryBuilder = consulServiceDiscoveryBuilder();
        consulServiceDiscoveryBuilder.setDatacenter(dataCenter);
        serviceDiscoverySupplier = consulServiceDiscoveryBuilder::build;
        return this;
    }

    /**
     * Enable DNS discovery
     *
     * @return fluent, this
     */
    public ManagedServiceBuilder enableDNSDiscovery() {
        serviceDiscoverySupplier = DnsUtil::createDnsServiceDiscovery;
        return this;
    }

    /**
     * Enable consul discovery.
     *
     * @param dataCenter name of datacenter we are connecting to.
     * @param host       name of host we are connecting to
     * @return fluent, this
     */
    public ManagedServiceBuilder enableConsulServiceDiscovery(final String dataCenter, final String host) {
        final ConsulServiceDiscoveryBuilder consulServiceDiscoveryBuilder = consulServiceDiscoveryBuilder();
        consulServiceDiscoveryBuilder.setDatacenter(dataCenter);
        consulServiceDiscoveryBuilder.setConsulHost(host);
        serviceDiscoverySupplier = consulServiceDiscoveryBuilder::build;
        return this;
    }

    /**
     * Enable lokate discovery.
     *
     * @param configURIs config URIs
     * @return fluent, this
     */
    public ManagedServiceBuilder enableLokateDiscovery(final URI... configURIs) {
        serviceDiscoverySupplier = () -> LokateServiceDiscoveryProvider.createLokateServiceDiscovery(configURIs);
        return this;
    }

    /**
     * Enable lokate discovery.
     *
     * @param configURIs config URIs
     * @return fluent, this
     */
    public ManagedServiceBuilder enableLokateDiscovery(final List<URI> configURIs) {
        serviceDiscoverySupplier = () -> LokateServiceDiscoveryProvider.createLokateServiceDiscovery(configURIs);
        return this;
    }

    /**
     * Enable consul discovery.
     *
     * @param dataCenter name of datacenter we are connecting to.
     * @param host       name of host we are connecting to
     * @param port       consul port
     * @return fluent, this
     **/
    public ManagedServiceBuilder enableConsulServiceDiscovery(final String dataCenter,
                                                              final String host,
                                                              final int port) {
        final ConsulServiceDiscoveryBuilder consulServiceDiscoveryBuilder = consulServiceDiscoveryBuilder();
        consulServiceDiscoveryBuilder.setDatacenter(dataCenter);
        consulServiceDiscoveryBuilder.setConsulHost(host);
        consulServiceDiscoveryBuilder.setConsulPort(port);
        serviceDiscoverySupplier = consulServiceDiscoveryBuilder::build;
        return this;
    }

    public ManagedServiceBuilder enableConsulServiceDiscovery(final String dataCenter,
                                                              final URI uri) {
        final ConsulServiceDiscoveryBuilder consulServiceDiscoveryBuilder = consulServiceDiscoveryBuilder();
        consulServiceDiscoveryBuilder.setDatacenter(dataCenter);
        consulServiceDiscoveryBuilder.setConnectionUri(uri);
        serviceDiscoverySupplier = consulServiceDiscoveryBuilder::build;
        return this;
    }

    /**
     * Used to get access to the serviceDiscoverySupplier which could be null.
     *
     * @return Supplier
     */
    public Supplier<ServiceDiscovery> getServiceDiscoverySupplier() {
        return serviceDiscoverySupplier;
    }

    /**
     * Used to inject a service discovery supplier.
     *
     * @param serviceDiscoverySupplier serviceDiscoverySupplier
     * @return fluent, this
     */
    public ManagedServiceBuilder setServiceDiscoverySupplier(Supplier<ServiceDiscovery> serviceDiscoverySupplier) {
        this.serviceDiscoverySupplier = serviceDiscoverySupplier;
        return this;
    }

    /**
     * Used to get the service discovery.
     *
     * @return ServiceDiscovery
     */
    public ServiceDiscovery getServiceDiscovery() {
        if (serviceDiscovery == null) {
            if (serviceDiscoverySupplier != null) {
                return serviceDiscoverySupplier.get();
            }
        }
        return serviceDiscovery;
    }

    /**
     * Used to inject service discovery.
     *
     * @param serviceDiscovery service discovery
     * @return fluent, this
     */
    public ManagedServiceBuilder setServiceDiscovery(ServiceDiscovery serviceDiscovery) {
        this.serviceDiscovery = serviceDiscovery;
        return this;
    }

    /**
     * Used to get the StatsD replicator builder.
     * You can use this to configure StatsD.
     * You can also use environment variables {@code STATSD_PORT} and {@code STATSD_HOST}.
     *
     * @return stats replicator builder.
     */
    public StatsDReplicatorBuilder getStatsDReplicatorBuilder() {
        if (statsDReplicatorBuilder == null) {
            statsDReplicatorBuilder = StatsDReplicatorBuilder.statsDReplicatorBuilder();

            final String statsDPort = System.getenv("STATSD_PORT");
            if (statsDPort != null && !statsDPort.isEmpty()) {
                statsDReplicatorBuilder.setPort(Integer.parseInt(statsDPort));
            }

            final String statsDHost = System.getenv("STATSD_HOST");
            if (statsDHost != null && !statsDHost.isEmpty()) {
                statsDReplicatorBuilder.setHost(statsDHost);
            }

        }
        return statsDReplicatorBuilder;
    }

    /**
     * Sets the statsD replicator.
     *
     * @param statsDReplicatorBuilder statsDReplicatorBuilder
     * @return fluent, this
     */
    public ManagedServiceBuilder setStatsDReplicatorBuilder(
            final StatsDReplicatorBuilder statsDReplicatorBuilder) {
        this.statsDReplicatorBuilder = statsDReplicatorBuilder;
        return this;
    }

    /**
     * Finds the admin port.
     * Searches  under environment variables,
     * {@code QBIT_ADMIN_PORT}, {@code ADMIN_PORT}, {@code PORT1}.
     *
     * @return admin port
     */
    public String findAdminPort() {

        String qbitAdminPort = System.getenv("QBIT_ADMIN_PORT");

        if (Str.isEmpty(qbitAdminPort)) {
            qbitAdminPort = System.getenv("ADMIN_PORT");
        }

        /* Uses PORT1 for admin port for Mesosphere / Heroku like environments. */
        if (Str.isEmpty(qbitAdminPort)) {
            qbitAdminPort = System.getenv("PORT1");
        }
        return qbitAdminPort;
    }

    /**
     * Get the Admin builder.
     *
     * @return admin builder.
     */
    public AdminBuilder getAdminBuilder() {
        if (adminBuilder == null) {
            adminBuilder = AdminBuilder.adminBuilder();

            final String qbitAdminPort = findAdminPort();
            if (qbitAdminPort != null && !qbitAdminPort.isEmpty()) {
                adminBuilder.setPort(Integer.parseInt(qbitAdminPort));
            }
            adminBuilder.setContextBuilder(this.getContextMetaBuilder());
            adminBuilder.setHealthService(getHealthService());
            adminBuilder.registerJavaVMStatsJob(getStatServiceBuilder().buildStatsCollector());

        }
        return adminBuilder;
    }

    /**
     * @param adminBuilder adminBuilder
     * @return this, fluent
     */
    public ManagedServiceBuilder setAdminBuilder(AdminBuilder adminBuilder) {
        this.adminBuilder = adminBuilder;
        return this;
    }

    /**
     * Get context meta builder.
     *
     * @return context meta builder
     */
    public ContextMetaBuilder getContextMetaBuilder() {
        if (contextMetaBuilder == null) {
            contextMetaBuilder = ContextMetaBuilder.contextMetaBuilder();
            contextMetaBuilder
                    .setHostAddress(this.getPublicHost() + ":" + this.getPublicPort())
                    .setRootURI(this.getRootURI());
        }
        return contextMetaBuilder;
    }

    /**
     * Get the context meta builder. Used for things like swagger generation.
     *
     * @param contextMetaBuilder contextMetaBuilder
     * @return this, fluent
     */
    public ManagedServiceBuilder setContextMetaBuilder(final ContextMetaBuilder contextMetaBuilder) {
        this.contextMetaBuilder = contextMetaBuilder;
        return this;
    }

    /**
     * Services with alias.
     *
     * @return map of services and their alias.
     */
    public Map<String, Object> getEndpointServiceMapWithAlias() {
        if (endpointServiceMapWithAlias == null) {
            endpointServiceMapWithAlias = new HashMap<>();
        }
        return endpointServiceMapWithAlias;
    }

    /**
     * Set in a map of alias
     *
     * @param endpointServiceMapWithAlias endpointServiceMapWithAlias
     * @return this, fluent
     */
    public ManagedServiceBuilder setEndpointServiceMapWithAlias(Map<String, Object> endpointServiceMapWithAlias) {
        this.endpointServiceMapWithAlias = endpointServiceMapWithAlias;
        return this;
    }

    /**
     * get the list of endpoint services
     *
     * @return services without alias
     */
    public List<Object> getEndpointServices() {
        if (endpointServices == null) {
            endpointServices = new ArrayList<>();
        }

        return endpointServices;
    }

    /**
     * Set in the list of services
     *
     * @param endpointServices endpoint services w/o alias
     * @return this, fluent
     */
    public ManagedServiceBuilder setEndpointServices(final List<Object> endpointServices) {
        this.endpointServices = endpointServices;
        return this;
    }

    /**
     * Add an endpoint with a managment bundle
     *
     * @param endpointService         endpoint service
     * @param serviceManagementBundle management bundle
     * @return this, fluent
     */
    public ManagedServiceBuilder addEndpointServiceWithServiceManagmentBundle(
            final Object endpointService, final ServiceManagementBundle serviceManagementBundle) {

        addEndpointServiceWithAliasAndQueueHandlerCallbacks(null, endpointService, new QueueCallBackHandler() {
            @Override
            public void queueProcess() {
                serviceManagementBundle.process();
            }
        });
        return this;
    }

    /**
     * Add an endpoint with a managment bundle and an alias
     *
     * @param alias                   alias
     * @param endpointService         endpoint service
     * @param serviceManagementBundle management bundle
     * @return this, fluent
     */
    public ManagedServiceBuilder addEndpointServiceWithAliasAndServiceManagmentBundle(
            final String alias,
            final Object endpointService, final ServiceManagementBundle serviceManagementBundle) {

        addEndpointServiceWithAliasAndQueueHandlerCallbacks(alias, endpointService, new QueueCallBackHandler() {
            @Override
            public void queueProcess() {
                serviceManagementBundle.process();
            }
        });
        return this;
    }

    /**
     * Add an endpoint service
     *
     * @param endpointService endpoint service
     * @return this, fluent
     */
    public ManagedServiceBuilder addEndpointService(final Object endpointService) {
        getContextMetaBuilder().addService(endpointService.getClass());
        getEndpointServices().add(endpointService);
        return this;
    }

    /**
     * Add an end point service with queue callback handlers
     *
     * @param endpointService       endpoint Service
     * @param queueCallBackHandlers queue callback handlers
     * @return this, fluent
     */
    public ManagedServiceBuilder addEndpointServiceWithQueueHandlerCallbacks(final Object endpointService,
                                                                             final QueueCallBackHandler...
                                                                                queueCallBackHandlers) {
        getContextMetaBuilder().addService(endpointService.getClass());
        getManagedServiceDefinitionList().add(new ManagedServiceDefinition(null, endpointService,
                queueCallBackHandlers));
        return this;
    }

    /**
     * Add an end point service with queue callback handlers
     *
     * @param alias                 alias
     * @param endpointService       endpoint Service
     * @param queueCallBackHandlers queue callback handlers
     * @return this, fluent
     */
    public ManagedServiceBuilder addEndpointServiceWithAliasAndQueueHandlerCallbacks(final String alias,
                                                                                final Object endpointService,
                                                                                final QueueCallBackHandler...
                                                                                queueCallBackHandlers) {
        getContextMetaBuilder().addService(endpointService.getClass());
        getManagedServiceDefinitionList().add(new ManagedServiceDefinition(alias, endpointService,
                queueCallBackHandlers));
        return this;
    }

    /**
     * Add endpoint service with alias
     *
     * @param alias           alias
     * @param endpointService endpoint service
     * @return this, fluent
     */
    public ManagedServiceBuilder addEndpointService(final String alias, final Object endpointService) {
        getContextMetaBuilder().addService(alias, endpointService.getClass());
        getEndpointServiceMapWithAlias().put(alias, endpointService);
        return this;
    }

    /**
     * Get the base QBit Factory
     *
     * @return qbit factory
     */
    public Factory getFactory() {
        if (factory == null) {
            factory = QBit.factory();
        }
        return factory;
    }

    /**
     * Set the QBit Factory
     *
     * @param factory factory
     * @return this, fluent
     */
    public ManagedServiceBuilder setFactory(final Factory factory) {
        this.factory = factory;
        return this;
    }

    /**
     * Get event manager
     *
     * @return event manager
     */
    public EventManager getEventManager() {
        if (eventManager == null) {
            eventManager = getFactory().systemEventManager();
        }
        return eventManager;
    }

    /**
     * Set event manager
     *
     * @param eventManager eventManager
     * @return this, fluent
     */
    public ManagedServiceBuilder setEventManager(EventManager eventManager) {
        this.eventManager = eventManager;
        return this;
    }

    /**
     * @return HealthServiceBuilder
     */
    public HealthServiceBuilder getHealthServiceBuilder() {

        if (healthServiceBuilder == null) {
            healthServiceBuilder = HealthServiceBuilder.healthServiceBuilder();
        }
        return healthServiceBuilder;
    }

    /**
     * @param healthServiceBuilder healthServiceBuilder
     * @return this, fluent
     */
    public ManagedServiceBuilder setHealthServiceBuilder(final HealthServiceBuilder healthServiceBuilder) {
        this.healthServiceBuilder = healthServiceBuilder;
        return this;
    }

    /**
     * @return http server builder
     */
    public HttpServerBuilder getHttpServerBuilder() {
        if (httpServerBuilder == null) {
            httpServerBuilder = HttpServerBuilder.httpServerBuilder();

            int port = getPort();

            if (!Str.isEmpty(port)) {
                httpServerBuilder.setPort(port);
            }
        }
        return httpServerBuilder;
    }

    /**
     * @param httpServerBuilder httpServerBuilder
     * @return this, fluent
     */
    public ManagedServiceBuilder setHttpServerBuilder(HttpServerBuilder httpServerBuilder) {
        this.httpServerBuilder = httpServerBuilder;
        return this;
    }

    /**
     * @return is statsD enabled.
     */
    public boolean isEnableStatsD() {
        return enableStatsD;
    }

    /**
     * @param enableStatsD enable statsD
     * @return this, fluent
     */
    public ManagedServiceBuilder setEnableStatsD(boolean enableStatsD) {
        this.enableStatsD = enableStatsD;
        return this;
    }

    /**
     * Enable statsD using the URI specified.
     *
     * @param uri uri for stats D
     * @return this, fluent
     */
    public ManagedServiceBuilder enableStatsD(final URI uri) {
        this.enableStatsD = true;
        this.getStatsDReplicatorBuilder().setHost(uri.getHost()).setPort(uri.getPort());
        return this;
    }

    /**
     * Enable statsD using the URI specified.
     *
     * @param host host for statsD
     * @param port port for statsD
     * @return this, fluent
     */
    public ManagedServiceBuilder enableStatsD(final String host, final int port) {
        this.enableStatsD = true;
        this.getStatsDReplicatorBuilder().setHost(host).setPort(port);
        return this;
    }

    public LocalStatsCollectorBuilder getLocalStatsCollectorBuilder() {
        if (localStatsCollectorBuilder == null) {
            localStatsCollectorBuilder = LocalStatsCollectorBuilder.localStatsCollectorBuilder();
        }
        return localStatsCollectorBuilder;
    }

    public ManagedServiceBuilder setLocalStatsCollectorBuilder(LocalStatsCollectorBuilder localStatsCollectorBuilder) {
        this.localStatsCollectorBuilder = localStatsCollectorBuilder;
        return this;
    }

    public boolean isEnableLocalStats() {
        return enableLocalStats;
    }

    public ManagedServiceBuilder setEnableLocalStats(boolean enableLocalStats) {
        this.enableLocalStats = enableLocalStats;
        return this;
    }

    public boolean isEnableLocalHealth() {
        return enableLocalHealth;
    }

    public ManagedServiceBuilder setEnableLocalHealth(boolean enableLocalHealth) {
        this.enableLocalHealth = enableLocalHealth;
        return this;
    }

    public QBitSystemManager getSystemManager() {

        if (systemManager == null) {
            systemManager = new QBitSystemManager();
        }
        return systemManager;
    }

    public ManagedServiceBuilder setSystemManager(QBitSystemManager systemManager) {
        this.systemManager = systemManager;
        return this;
    }

    public StatServiceBuilder getStatServiceBuilder() {
        if (statServiceBuilder == null) {
            statServiceBuilder = StatServiceBuilder.statServiceBuilder();

            if (enableLocalStats) {
                statServiceBuilder.addReplicator(getLocalStatsCollectorBuilder().buildAndStart());
            }

            if (enableStatsD) {
                statServiceBuilder.addReplicator(getStatsDReplicatorBuilder().buildAndStart());
            }

            statServiceBuilder.build();
            statServiceBuilder.buildServiceQueueWithCallbackHandler()
                    .startCallBackHandler().start();

        }
        return statServiceBuilder;
    }

    public ManagedServiceBuilder setStatServiceBuilder(StatServiceBuilder statServiceBuilder) {

        this.statServiceBuilder = statServiceBuilder;
        return this;
    }

    public EndpointServerBuilder getEndpointServerBuilder() {
        if (endpointServerBuilder == null) {
            endpointServerBuilder = EndpointServerBuilder.endpointServerBuilder();
            endpointServerBuilder.setPort(this.getPort());
            endpointServerBuilder.setEnableHealthEndpoint(isEnableLocalHealth());
            endpointServerBuilder.setEnableStatEndpoint(isEnableLocalStats());

            endpointServerBuilder.setHealthService(getHealthService());
            endpointServerBuilder.setSystemManager(this.getSystemManager());
            endpointServerBuilder.setHttpServerBuilder(getHttpServerBuilder());
            endpointServerBuilder.setStatsFlushRateSeconds(getSampleStatFlushRate());
            endpointServerBuilder.setCheckTimingEveryXCalls(getCheckTimingEveryXCalls());
            endpointServerBuilder.setServiceDiscovery(getServiceDiscovery());
            endpointServerBuilder.setUri(getRootURI());
            endpointServerBuilder.setEventManager(this.getEventManager());

            configureEndpointServerBuilderForInterceptors(endpointServerBuilder);

            if (isEnableStats()) {

                endpointServerBuilder.setStatsCollector(getStatServiceBuilder().buildStatsCollectorWithAutoFlush());
            }

            if (isEnableLocalStats()) {
                endpointServerBuilder.setEnableStatEndpoint(true);
                endpointServerBuilder.setStatsCollection(getLocalStatsCollectorBuilder().build());
            }

            endpointServerBuilder.setupHealthAndStats(getHttpServerBuilder());

            if (endpointServices != null) {
                endpointServerBuilder.setServices(endpointServices);
            }

            if (endpointServiceMapWithAlias != null) {
                endpointServerBuilder.setServicesWithAlias(endpointServiceMapWithAlias);
            }

        }

        return endpointServerBuilder;
    }

    public ManagedServiceBuilder setEndpointServerBuilder(EndpointServerBuilder endpointServerBuilder) {
        this.endpointServerBuilder = endpointServerBuilder;
        return this;
    }

    private void configureEndpointServerBuilderForInterceptors(final EndpointServerBuilder endpointServerBuilder) {

        final Interceptors interceptors = configureInterceptors();
        if (interceptors.before.size() > 0) {
            endpointServerBuilder.setBeforeMethodCallOnServiceQueue(new BeforeMethodCallChain(interceptors.before));
        }
        if (interceptors.after.size() > 0) {
            endpointServerBuilder.setAfterMethodCallOnServiceQueue(new AfterMethodCallChain(interceptors.after));
        }
        if (interceptors.beforeSent.size() > 0) {
            endpointServerBuilder.setBeforeMethodSent(new BeforeMethodSentChain(interceptors.beforeSent));
        }
    }

    private void configureServiceBundleBuilderForInterceptors(final ServiceBundleBuilder serviceBundleBuilder) {

        final Interceptors interceptors = configureInterceptors();
        if (interceptors.before.size() > 0) {
            serviceBundleBuilder.setBeforeMethodCallOnServiceQueue(new BeforeMethodCallChain(interceptors.before));
        }
        if (interceptors.after.size() > 0) {
            serviceBundleBuilder.setAfterMethodCallOnServiceQueue(new AfterMethodCallChain(interceptors.after));
        }
        if (interceptors.beforeSent.size() > 0) {
            serviceBundleBuilder.setBeforeMethodSent(new BeforeMethodSentChain(interceptors.beforeSent));
        }
    }

    private void configureServiceBuilderForInterceptors(final ServiceBuilder serviceBuilder) {

        final Interceptors interceptors = configureInterceptors();
        if (interceptors.before.size() > 0) {
            serviceBuilder.setBeforeMethodCall(new BeforeMethodCallChain(interceptors.before));
        }
        if (interceptors.after.size() > 0) {
            serviceBuilder.setAfterMethodCall(new AfterMethodCallChain(interceptors.after));
        }
        if (interceptors.beforeSent.size() > 0) {
            serviceBuilder.setBeforeMethodSent(new BeforeMethodSentChain(interceptors.beforeSent));
        }
    }

    /**
     * Get the ServiceEndpointServer constructed with all of the service endpionts that
     * you registered
     *
     * @return new ServiceEndpointServer.
     */
    public ServiceEndpointServer getServiceEndpointServer() {

        final ServiceEndpointServer serviceEndpointServer = getEndpointServerBuilder().build();

        if (managedServiceDefinitionList != null) {
            managedServiceDefinitionList.forEach(serviceDef -> {
                if (serviceDef.getAlias() == null) {
                    serviceEndpointServer.addServiceWithQueueCallBackHandlers(serviceDef.getServiceObject(),
                            serviceDef.getQueueCallBackHandlers());
                } else {
                    serviceEndpointServer.addServiceObjectWithQueueCallBackHandlers(serviceDef.getAlias(),
                            serviceDef.getServiceObject(), serviceDef.getQueueCallBackHandlers());
                }
            });
        }
        return serviceEndpointServer;

    }

    /**
     * Starts up the application.
     * @return service endpoint server
     */
    public ServiceEndpointServer startApplication() {
        return getServiceEndpointServer().startServerAndWait();
    }

    /**
     * Configure a list of common interceptors.
     *
     * @return interceptors.
     */
    private Interceptors configureInterceptors() {
        Interceptors interceptors = new Interceptors();
        SetupMdcForHttpRequestInterceptor setupMdcForHttpRequestInterceptor;
        if (enableLoggingMappedDiagnosticContext) {
            enableRequestChain = true;
            if (requestHeadersToTrackForMappedDiagnosticContext != null &&
                    requestHeadersToTrackForMappedDiagnosticContext.size() > 0) {
                setupMdcForHttpRequestInterceptor =
                        new SetupMdcForHttpRequestInterceptor(requestHeadersToTrackForMappedDiagnosticContext);
            } else {
                setupMdcForHttpRequestInterceptor = new SetupMdcForHttpRequestInterceptor(Collections.emptySet());
            }
            interceptors.before.add(setupMdcForHttpRequestInterceptor);
            interceptors.after.add(setupMdcForHttpRequestInterceptor);
        }

        if (enableRequestChain) {
            final CaptureRequestInterceptor captureRequestInterceptor = new CaptureRequestInterceptor();
            interceptors.before.add(captureRequestInterceptor);
            interceptors.after.add(captureRequestInterceptor);
            interceptors.beforeSent.add(new ForwardCallMethodInterceptor(new RequestContext()));
        }
        return interceptors;
    }

    public EndpointServerBuilder createEndpointServerBuilder() {

        EndpointServerBuilder endpointServerBuilder = EndpointServerBuilder.endpointServerBuilder();
        endpointServerBuilder.setEnableHealthEndpoint(isEnableLocalHealth());
        endpointServerBuilder.setSystemManager(this.getSystemManager());
        endpointServerBuilder.setHealthService(getHealthService());
        endpointServerBuilder.setStatsFlushRateSeconds(getSampleStatFlushRate());
        endpointServerBuilder.setCheckTimingEveryXCalls(getCheckTimingEveryXCalls());
        endpointServerBuilder.setServiceDiscovery(getServiceDiscovery());
        endpointServerBuilder.setEventManager(this.getEventManager());

        configureEndpointServerBuilderForInterceptors(endpointServerBuilder);

        if (isEnableStats()) {

            endpointServerBuilder.setStatsCollector(getStatServiceBuilder().buildStatsCollectorWithAutoFlush());
        }

        if (isEnableLocalStats()) {
            endpointServerBuilder.setEnableStatEndpoint(true);
            endpointServerBuilder.setStatsCollection(getLocalStatsCollectorBuilder().build());
        }

        return endpointServerBuilder;
    }

    public ServiceBundleBuilder createServiceBundleBuilder() {

        ServiceBundleBuilder serviceBundleBuilder = ServiceBundleBuilder.serviceBundleBuilder();
        serviceBundleBuilder.setSystemManager(this.getSystemManager());
        serviceBundleBuilder.setHealthService(getHealthService());
        serviceBundleBuilder.setCheckTimingEveryXCalls(this.getCheckTimingEveryXCalls());
        serviceBundleBuilder.setStatsFlushRateSeconds(this.getSampleStatFlushRate());
        serviceBundleBuilder.setEventManager(this.getEventManager());

        configureServiceBundleBuilderForInterceptors(serviceBundleBuilder);

        if (isEnableStats()) {
            serviceBundleBuilder.setStatsCollector(getStatServiceBuilder().buildStatsCollector());
        }

        return serviceBundleBuilder;
    }

    public ServiceBuilder createServiceBuilderForServiceObject(final Object serviceObject) {

        final ServiceBuilder serviceBuilder = ServiceBuilder.serviceBuilder();
        serviceBuilder.setSystemManager(this.getSystemManager());
        serviceBuilder.setEventManager(this.getEventManager());

        serviceBuilder.setServiceObject(serviceObject);

        final String bindStatHealthName = AnnotationUtils.readServiceName(serviceObject);

        if (isEnableLocalHealth()) {
            serviceBuilder.registerHealthChecks(getHealthService(), bindStatHealthName);
        }

        configureServiceBuilderForInterceptors(serviceBuilder);

        if (isEnableStats()) {

            serviceBuilder.registerStatsCollections(bindStatHealthName,
                    getStatServiceBuilder().buildStatsCollector(), getSampleStatFlushRate(),
                    getCheckTimingEveryXCalls());
        }

        return serviceBuilder;

    }

    public HealthServiceAsync getHealthService() {

        if (healthServiceBuilder == null) {
            if (healthService == null) {
                HealthServiceBuilder builder = getHealthServiceBuilder();
                healthService = builder.setAutoFlush().buildAndStart();
            }

            return healthService;
        } else {
            return healthServiceBuilder.buildHealthSystemReporter();
        }
    }

    public ManagedServiceBuilder setHealthService(HealthServiceAsync healthServiceAsync) {
        this.healthService = healthServiceAsync;
        return this;
    }

    public boolean isEnableStats() {
        return enableStats;
    }

    public ManagedServiceBuilder setEnableStats(boolean enableStats) {
        this.enableStats = enableStats;
        return this;
    }

    public int getSampleStatFlushRate() {
        return sampleStatFlushRate;
    }

    public ManagedServiceBuilder setSampleStatFlushRate(int sampleStatFlushRate) {
        this.sampleStatFlushRate = sampleStatFlushRate;
        return this;
    }

    public int getCheckTimingEveryXCalls() {
        return checkTimingEveryXCalls;
    }

    public ManagedServiceBuilder setCheckTimingEveryXCalls(int checkTimingEveryXCalls) {
        this.checkTimingEveryXCalls = checkTimingEveryXCalls;
        return this;
    }

    /**
     * Sets up DNS based service discovery.
     *
     * @return fluent, this
     */
    public ManagedServiceBuilder useDnsServiceDiscovery() {
        this.setServiceDiscovery(DnsUtil.createDnsServiceDiscovery());
        return this;
    }

    /**
     * Create a new StatsCollector
     *
     * @return new stats collector for a single service.
     */
    public StatsCollector createStatsCollector() {
        return this.getStatServiceBuilder().buildStatsCollector();
    }

    /**
     * Hold lists of interceptors.
     */
    private static class Interceptors {
        List<BeforeMethodCall> before = new ArrayList<>();
        List<AfterMethodCall> after = new ArrayList<>();
        List<BeforeMethodSent> beforeSent = new ArrayList<>();
    }

}


package io.advantageous.consul.endpoints;

import io.advantageous.consul.domain.Session;
import io.advantageous.consul.domain.SessionId;
import io.advantageous.consul.domain.option.RequestOptions;
import io.advantageous.qbit.http.HTTP;
import io.advantageous.qbit.http.request.HttpRequestBuilder;
import io.advantageous.qbit.json.JsonMapper;

import java.net.URI;
import java.util.List;
import java.util.Optional;

import static io.advantageous.consul.domain.ConsulException.die;

/**
 * Consul Session endpoint.
 */
public class SessionEndpoint extends Endpoint {

    public SessionEndpoint(URI rootURI, String rootPath, final JsonMapper mapper) {
        super(rootURI, rootPath, mapper);
    }

    /**
     * The create endpoint is used to initialize a new session.
     * Sessions must be associated with a node and may be associated with any number of checks.
     * The create endpoint expects a JSON request body to be PUT. The request body must look like:
     *
     *

     *
     * {
     * "LockDelay": "15s",
     * "Name": "my-service-lock",
     * "Node": "foobar",
     * "Checks": ["a", "b", "c"],
     * "Behavior": "release",
     * "TTL": "0s"
     * }
     *
     *

     *
     * None of the fields are mandatory, and in fact no body needs to be PUT if the defaults are to be used.
     * By default, the agent's local datacenter is used; another datacenter can be specified using
     * the "?dc=" query parameter. However, it is not recommended to use cross-datacenter sessions.
     * /v1/session/create: Creates a new session
     *
     * @param session    session to create
     * @param datacenter datacenter
     * @return a session id
     */
    public String create(Session session, final String datacenter) {

        final URI uri = createURI("/create");
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(datacenter, null, null, "");

        HTTP.Response httpResponse = HTTP.jsonRestCallViaPUT(uri.toString() + "?" + httpRequestBuilder.paramString(),
                toJson(session));

        if (httpResponse == null || httpResponse.code() != 200) {
            die("Unable to create the session", uri, httpResponse);
        }

        final String id = fromJson(httpResponse.body(), SessionId.class).getId();
        session.setId(id);
        return id;
    }

    /**
     * @param session session
     * @return sessionId session id
     */
    public String create(Session session) {

        return create(session, null);
    }

    /**
     * The destroy endpoint is hit with a PUT and destroys the given session.
     *

     * By default, the local datacenter is used, but the "?dc=" query parameter
     * can be used to specify the datacenter.
     *

     * The session being destroyed must be provided on the path.
     * The return code is 200 on success.
     * /v1/session/destroy/session: Destroys a given session
     * @param session  session to destroy.
     * @param sessionId sessionId
     * @return true if success
     */
    public boolean destroy(String sessionId, final Session session) {

        return destroy(sessionId, session, null);
    }

    /**
     * Destroy the session.
     * @param sessionId id of session
     * @param session session
     * @param datacenter datacenter
     * @return true if success
     */
    public boolean destroy(final String sessionId, final Session session, final String datacenter) {

        final URI uri = createURI("/destroy/" + sessionId);
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(datacenter, null, null, "");

        HTTP.Response httpResponse = HTTP.jsonRestCallViaPUT(uri.toString() + "?" + httpRequestBuilder.paramString(),
                toJson(session));

        if (httpResponse == null || httpResponse.code() != 200) {
            die("Unable destroy the session", sessionId, uri, httpResponse);
        }

        return httpResponse.code() == 200;
    }

    /**
     * List all active sessions
     *
     * @return active sessions
     */
    public List<Session> getSessions() {

        return getSessions(null, RequestOptions.BLANK);
    }

    /**
     * Lists all active sessions
     * /v1/session/list:
     *

     * This endpoint is hit with a GET and returns the active sessions for a given datacenter. By default,
     * the datacenter of the agent is queried; however, the dc can be provided using the "?dc=" query parameter.
     * This endpoint supports blocking queries and all consistency modes.
     *
     * @param requestOptions request options for consistency and long poll
     * @return list of sessions
     */
    public List<Session> getSessions(final RequestOptions requestOptions) {

        return getSessions(null, requestOptions);
    }

    /**
     * @param datacenter     datacenter
     * @param requestOptions request options for long poll and consistency.
     * @return list of sessions
     */
    public List<Session> getSessions(final String datacenter, final RequestOptions requestOptions) {

        final URI uri = createURI("/list");
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(datacenter, null, requestOptions, "");

        HTTP.Response httpResponse = HTTP.getResponse(uri.toString() + "?" + httpRequestBuilder.paramString());

        if (httpResponse == null || httpResponse.code() != 200) {
            die("Unable to get the sessions", uri, httpResponse);
        }

        return fromJsonArray(httpResponse.body(), Session.class);

    }

    /**
     * The renew endpoint is hit with a PUT and renews the given session.
     * This is used with sessions that have a TTL, and it extends the expiration by the TTL.
     * By default, the local datacenter is used, but the "?dc=" query parameter can be used to
     * specify the datacenter.
     *

     * The session being renewed must be provided on the path.
     *

     * The return code is 200 on success.
     * The response JSON body is a single Session in a list.
     *

     * /v1/session/renew: Renews a TTL-based session
     *
     * @param sessionId session id
     *
     * @return session
     */
    public Session renew(final String sessionId) {

        return renew(sessionId, null);
    }

    /**
     * /v1/session/renew: Renews a TTL-based session
     *
     * @param sessionId  session id
     * @param datacenter datacenter
     * @return session
     */
    public Session renew(final String sessionId, final String datacenter) {

        final URI uri = createURI("/renew/" + sessionId);
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(datacenter, null, null, "");

        HTTP.Response httpResponse = HTTP.jsonRestCallViaPUT(uri.toString() + "?" + httpRequestBuilder.paramString(),
                "");

        if (httpResponse == null || httpResponse.code() != 200) {
            die("Unable to renew the session", uri, httpResponse);
        }

        return fromJsonArray(httpResponse.body(), Session.class).get(0);
    }

    /**
     * This endpoint is hit with a GET and returns the requested session information
     * within a given datacenter. By default, the datacenter of the agent is queried; however,
     * the dc can be provided using the "?dc=" query parameter.
     *

     * The session being queried must be provided on the path.
     * It returns a JSON body like this:
     *

     *
     * [{
     * "LockDelay": 1.5e+10,
     * "Checks": [
     * "serfHealth"
     * ],
     * "Node": "foobar",
     * "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e",
     * "CreateIndex": 1086449
     * }]
     *
     *

     *

     * If the session is not found, null is returned instead of a JSON list.
     * This endpoint supports blocking queries and all consistency modes.
     *

     * /v1/session/info/session: Queries a given session
     *
     * @param sessionId      id of a session
     * @param requestOptions request options
     * @return Session for this session id.
     */
    public Optional<Session> info(final String sessionId, RequestOptions requestOptions) {

        return info(sessionId, null, requestOptions);

    }

    public Optional<Session> info(final String sessionId) {

        return info(sessionId, null, RequestOptions.BLANK);

    }

    /**
     * /v1/session/info/session:
     * Queries a given session
     *
     * @param sessionId      id of a session
     * @param requestOptions request options
     * @param datacenter     datacenter
     * @return Session for this session id.
     */
    public Optional<Session> info(final String sessionId, final String datacenter, final RequestOptions requestOptions) {

        final URI uri = createURI("/info/" + sessionId);
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(datacenter, null, null, "");

        HTTP.Response httpResponse = HTTP.getResponse(uri.toString() + "?" + httpRequestBuilder.paramString());

        if (httpResponse == null || httpResponse.code() != 200) {
            die("Unable to get the sessions", uri, httpResponse);
        }

        final List<Session> sessions = fromJsonArray(httpResponse.body(), Session.class);
        if (sessions == null || sessions.size() == 0) {
            return Optional.empty();
        }

        return Optional.of(sessions.get(0));

    }

    /**
     * This endpoint is hit with a GET and returns the active sessions for a given node and datacenter.
     * By default, the datacenter of the agent is queried; however,
     * the dc can be provided using the "?dc=" query parameter.
     * The node being queried must be provided on the path.
     * /v1/session/node/node:
     * Lists sessions belonging to a node
     *
     * @param node           node
     * @param requestOptions request options for consistency and long poll
     * @return list of sessions for this node
     */
    public List<Session> getSessionsForNode(final String node, final RequestOptions requestOptions) {

        return getSessionsForNode(node, null, requestOptions);
    }

    public List<Session> getSessionsForNode(final String node) {
        return getSessionsForNode(node, null, RequestOptions.BLANK);
    }

    /**
     * /v1/session/node/node:
     * Lists sessions belonging to a node
     *
     * @param node node
     * @param requestOptions request options for consistency and long poll
     * @param datacenter     data center param
     * @return list of sessions for this node
     */
    public List<Session> getSessionsForNode(final String node, final String datacenter, final RequestOptions requestOptions) {

        final URI uri = createURI("/node/" + node);
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(datacenter, null, requestOptions, "");

        HTTP.Response httpResponse = HTTP.getResponse(uri.toString() + "?" + httpRequestBuilder.paramString());

        if (httpResponse == null || httpResponse.code() != 200) {
            die("Unable to get the sessions", uri, httpResponse);
        }

        return fromJsonArray(httpResponse.body(), Session.class);
    }

}


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

import io.advantageous.qbit.reactive.CallbackBuilder;
import io.advantageous.qbit.service.discovery.EndpointDefinition;
import io.advantageous.qbit.service.discovery.spi.ServiceDiscoveryProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Wraps DnsSupport to fit in as a ServiceDiscoveryProvider.
 */
public class DnsServiceDiscoveryProvider implements ServiceDiscoveryProvider {

    /**
     * Dns support talks to DNS server to get EndpointDefinitions.
     */
    private final DnsSupport dnsSupport;

    /**
     * Timeout for DNS call.
     */
    private final int timeout;

    /**
     * Time unit for timeout.
     */
    private final TimeUnit timeUnit;

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

    private final boolean debug = logger.isDebugEnabled();

    /**
     * New DnsServiceDiscoveryProvider.
     *
     * @param dnsSupport dnsSupport
     * @param timeout    timeout
     * @param timeUnit   timeUnit
     */
    public DnsServiceDiscoveryProvider(final DnsSupport dnsSupport,
                                       final int timeout,
                                       final TimeUnit timeUnit) {
        this.dnsSupport = dnsSupport;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
    }

    /**
     * Load the services.
     *
     * @param serviceName serviceName
     * @return list of EndpointDefinition
     */
    @Override
    public List<EndpointDefinition> loadServices(final String serviceName) {

        if (debug) logger.debug("Loading Service {}", serviceName);

        final CountDownLatch countDownLatch = new CountDownLatch(1);

        final AtomicReference<List<EndpointDefinition>> endPointsRef = new AtomicReference<>();

        final AtomicReference<Throwable> exceptionAtomicReference = new AtomicReference<>();

        dnsSupport.loadServiceEndpointsByServiceName(CallbackBuilder.newCallbackBuilder()
                .withListCallback(EndpointDefinition.class,
                        endpointDefinitions ->
                        {

                            endPointsRef.set(endpointDefinitions);
                            countDownLatch.countDown();

                        }).withErrorHandler(exceptionAtomicReference::set)
                .build(), serviceName);

        try {
            if (debug) logger.debug("Waiting for load services {} {}", timeout, timeUnit);

            countDownLatch.await(timeout, timeUnit);
        } catch (InterruptedException e) {
            throw new IllegalStateException("DNS Timeout", e);
        }

        if (exceptionAtomicReference.get() != null) {
            logger.error("DnsServiceDiscoveryProvider.loadServices EXCEPTION", exceptionAtomicReference.get());
            throw new IllegalStateException("Unable to read from DNS", exceptionAtomicReference.get());
        } else {

            if (debug) logger.debug("DnsServiceDiscoveryProvider.loadServices SUCCESS");
            return endPointsRef.get();
        }

    }

}


/*
 * 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!
 *
 * Note this class was heavily influenced and inspired by the Orbitz Consul client.
 */
package io.advantageous.consul.endpoints;

import io.advantageous.boon.core.Str;
import io.advantageous.consul.domain.KeyValue;
import io.advantageous.consul.domain.option.KeyValuePutOptions;
import io.advantageous.consul.domain.option.RequestOptions;
import io.advantageous.qbit.http.HTTP;
import io.advantageous.qbit.http.request.HttpRequestBuilder;
import io.advantageous.qbit.http.request.HttpTextResponse;
import io.advantageous.qbit.json.JsonMapper;

import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;

import static io.advantageous.consul.domain.ConsulException.die;
import static io.advantageous.consul.endpoints.RequestUtils.decodeBase64;

/**
 * HTTP Client for /v1/kv/ endpoints.
 *

 * Note this class was heavily influenced and inspired by the Orbitz Consul client.
 */
public class KeyValueStoreEndpoint extends Endpoint {

    public KeyValueStoreEndpoint(String scheme, String host, String port, String rootPath, JsonMapper mapper) {
        super(scheme, host, port, rootPath, mapper);
    }

    public KeyValueStoreEndpoint(URI rootURI, String rootPath, JsonMapper mapper) {
        super(rootURI, rootPath, mapper);
    }

    /**
     * Retrieves a {@link io.advantageous.consul.domain.KeyValue} for a specific key
     * from the key/value store.
     *

     * GET /v1/keyValueStore/{key}
     *
     * @param key The key to retrieve.
     * @return An {@link Optional} containing the value or {@link java.util.Optional#empty()}
     */
    public Optional<KeyValue> getValue(String key) {
        return getValue(key, RequestOptions.BLANK);
    }

    /**
     * Retrieves a {@link io.advantageous.consul.domain.KeyValue} for a specific key
     * from the key/value store.
     *

     * GET /v1/keyValueStore/{key}
     *
     * @param key            The key to retrieve.
     * @param requestOptions The query options.
     * @return An {@link Optional} containing the value or {@link java.util.Optional#empty()}
     */
    public Optional<KeyValue> getValue(final String key, RequestOptions requestOptions) {

        final URI uri = createURI("/" + key);

        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(null, null, requestOptions, "");

        final HTTP.Response httpResponse = HTTP.getResponse(uri.toString() + "?" + httpRequestBuilder.paramString());

        if (httpResponse.code() == 404) {
            return Optional.empty();
        }

        if (httpResponse.code() != 200) {
            die("Unable to retrieve the key", key, uri, httpResponse.code(), httpResponse.body());
        }

        return getKeyValueOptional(httpResponse);
    }

    private Optional<KeyValue> getKeyValueOptional(HttpTextResponse httpResponse) {
        final List<KeyValue> keyValues = fromJsonArray(httpResponse.body(), KeyValue.class);

        return keyValues != null && keyValues.size() > 0 ? Optional.of(keyValues.get(0)) : Optional.<KeyValue>empty();
    }

    private Optional<KeyValue> getKeyValueOptional(HTTP.Response httpResponse) {
        final List<KeyValue> keyValues = fromJsonArray(httpResponse.body(), KeyValue.class);

        keyValues.forEach(keyValue -> keyValue.setValue(decodeBase64(keyValue.getValue())));

        return keyValues != null && keyValues.size() > 0 ? Optional.of(keyValues.get(0)) : Optional.<KeyValue>empty();
    }

    /**
     * Retrieves a list of {@link io.advantageous.consul.domain.KeyValue} objects for a specific key
     * from the key/value store.
     *

     * GET /v1/keyValueStore/{key}?recurse
     *
     * @param key The key to retrieve.
     * @return A list of zero to many {@link io.advantageous.consul.domain.KeyValue} objects.
     */
    public List<KeyValue> getValues(String key) {

        final URI uri = createURI("/" + key);
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(null, null, RequestOptions.BLANK, "");

        httpRequestBuilder.addParam("recurse", "true");

        final HTTP.Response httpResponse = HTTP.getResponse(uri.toString() + "?" + httpRequestBuilder.paramString());

        if (httpResponse.code() != 200) {
            die("Unable to retrieve the service", uri, httpResponse.code(), httpResponse.body());
        }

        return fromJsonArray(httpResponse.body(), KeyValue.class);

    }

    /**
     * Retrieves a string value for a specific key from the key/value store.
     *

     * GET /v1/keyValueStore/{key}
     *
     * @param key The key to retrieve.
     * @return An {@link Optional} containing the value as a string or
     * {@link java.util.Optional#empty()}
     */
    public Optional<String> getValueAsString(String key) {
        Optional<KeyValue> value = getValue(key);

        return value.isPresent() ? Optional.of(RequestUtils.decodeBase64(value.get().getValue()))
                : Optional.<String>empty();
    }

    /**
     * Retrieves a list of string values for a specific key from the key/value
     * store.
     *

     * GET /v1/keyValueStore/{key}?recurse
     *
     * @param key The key to retrieve.
     * @return A list of zero to many string values.
     */
    public List<String> getValuesAsString(String key) {
        @SuppressWarnings("UnnecessaryLocalVariable")
        List<String> result = getValues(key).stream().map(keyValue ->
                RequestUtils.decodeBase64(keyValue.getValue())).collect(Collectors.toList());
        return result;
    }

    /**
     * Puts a value into the key/value store.
     *
     * @param key   The key to use as index.
     * @param value The value to index.
     * @return true if the value was successfully indexed.
     */
    public boolean putValue(String key, String value) {
        return putValue(key, value, 0L, KeyValuePutOptions.BLANK);
    }

    /**
     * Puts a value into the key/value store.
     *
     * @param key   The key to use as index.
     * @param value The value to index.
     * @param flags The flags for this key.
     * @return true if the value was successfully indexed.
     */
    public boolean putValue(String key, String value, long flags) {
        return putValue(key, value, flags, KeyValuePutOptions.BLANK);
    }

    /**
     * Puts a value into the key/value store.
     *
     * @param key        The key to use as index.
     * @param value      The value to index.
     * @param putOptions PUT options (e.g. wait, acquire).
     * @param flags      flags
     * @return true if the value was successfully indexed.
     */
    public boolean putValue(final String key, final String value, final long flags, final KeyValuePutOptions putOptions) {
        Integer cas = putOptions.getCas();
        String release = putOptions.getRelease();
        String acquire = putOptions.getAcquire();

        final URI uri = createURI("/" + key);
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(null, null, RequestOptions.BLANK, "");

//        httpRequestBuilder.addParam("recurse", "true");

        if (cas != null) {
            httpRequestBuilder.addParam("cas", cas.toString());
        }

        if (!Str.isEmpty(release)) {
            httpRequestBuilder.addParam("release", release);
        }

        if (!Str.isEmpty(acquire)) {
            httpRequestBuilder.addParam("acquire", acquire);
        }

        if (flags != 0) {
            httpRequestBuilder.addParam("flags", String.valueOf(flags));
        }

        httpRequestBuilder.setBody(value);
        httpRequestBuilder.setMethodPut();

        final HTTP.Response httpResponse = HTTP.jsonRestCallViaPUT(uri.toString() + "?" + httpRequestBuilder.paramString(),
                value);

        if (httpResponse.code() == 200) {
            return Boolean.parseBoolean(httpResponse.body());
        } else {
            die("Unable to put value", uri, putOptions, httpResponse.code(), httpResponse.body());
            return false;
        }

    }

    /**
     * Retrieves a list of matching keys for the given key.
     *

     * GET /v1/keyValueStore/{key}?keys
     *
     * @param key The key to retrieve.
     * @return A list of zero to many keys.
     */
    public List<String> getKeys(String key) {

        final URI uri = createURI("/" + key);
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(null, null, RequestOptions.BLANK, "");

        httpRequestBuilder.addParam("keys", "true");

        final HTTP.Response httpResponse = HTTP.getResponse(uri.toString() + "?" + httpRequestBuilder.paramString());

        if (httpResponse.code() == 200) {
            return fromJsonArray(httpResponse.body(), String.class);
        } else {
            die("Unable to get nested keys", uri, key, httpResponse.code(), httpResponse.body());
            return Collections.emptyList();
        }
    }

    /**
     * Deletes a specified key.
     *

     * DELETE /v1/keyValueStore/{key}
     *
     * @param key The key to delete.
     */
    public void deleteKey(String key) {
        //noinspection unchecked,unchecked
        delete(key, Collections.EMPTY_MAP);
    }

    /**
     * Deletes a specified key and any below it.
     *

     * DELETE /v1/keyValueStore/{key}?recurse
     *
     * @param key The key to delete.
     */
    public void deleteKeys(String key) {
        delete(key, Collections.singletonMap("recurse", "true"));
    }

    /**
     * Deletes a specified key.
     *
     * @param key    The key to delete.
     * @param params Map of parameters, e.g. recurse.
     */
    private void delete(String key, Map<String, String> params) {

        final URI uri = createURI("/" + key);
        final HttpRequestBuilder httpRequestBuilder = RequestUtils
                .getHttpRequestBuilder(null, null, RequestOptions.BLANK, "");

        final Set<Map.Entry<String, String>> entries = params.entrySet();
        for (Map.Entry<String, String> entry : entries) {
            httpRequestBuilder.addParam(entry.getKey(), entry.getValue());
        }

        httpRequestBuilder.setMethodDelete();
        final HTTP.Response httpResponse = HTTP.deleteResponse(uri.toString() + "?" + httpRequestBuilder.paramString());

        if (httpResponse.code() != 200) {
            die("Unable to delete key", uri, key, httpResponse.code(), httpResponse.body());
        }
    }
}


package io.advantageous.qbit.stream;

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedTransferQueue;

/**
 * Turns a non-blocking reactive stream into a blocking iterator.
 * This is used to support an easier but less performance-oriented programming model.
 *
 * @param <T> T
 */
public class StreamToIterator<T> implements Iterator<T> {

    /**
     * Passes items from the non-blocking stream to the client thread where it can block.
     */
    private final BlockingQueue<Object> blockingQueue = new LinkedTransferQueue<>(); //Unbounded
    /**
     * Message to denote that the stream is finished.
     */
    private final Object COMPLETE = new Object();
    /**
     * Message to denote that the stream passed an exception, which we will wrap an re-throw.
     */
    private final Object ERROR = new Object();
    /**
     * Denotes that the stream sent the close message or sent an error.
     */
    private boolean complete;
    /**
     * Holds the next item in the queue.
     */
    private T next;

    /**
     * Creates Iterator that wraps the pulisher with a blocking interface.
     *
     * @param publisher pulisher that we are adapting as a blocking Iterator
     */
    private StreamToIterator(final Publisher<T> publisher, final int requestSize) {
        publisher.subscribe(new Subscriber<T>() {
            private Subscription subscription;
            private int count = requestSize;

            @Override
            public void onSubscribe(Subscription s) {

                this.subscription = s;
                this.subscription.request(requestSize);
            }

            @Override
            public void onNext(T item) {

                blockingQueue.offer(item);

                count--;
                if (count <= 0) {
                    count = requestSize;
                    subscription.request(requestSize);//this is settable
                }

            }

            @Override
            public void onError(final Throwable error) {

                blockingQueue.offer(ERROR);
                blockingQueue.offer(error);
            }

            @Override
            public void onComplete() {

                blockingQueue.offer(COMPLETE);

            }
        });
    }

    /**
     * @param publisher pulisher that we are adapting as a blocking Iterator
     * @param <T>       T
     * @return Iterator that wraps the pulisher with a blocking interface.
     */
    public static <T> Iterator<T> toIterator(final Publisher<T> publisher) {
        return new StreamToIterator<>(publisher, 2);
    }

    /**
     * @param publisher           pulisher that we are adapting as a blocking Iterator
     * @param numRequestsPipeline how many messages we will accept up to
     * @param <T>                 T
     * @return Iterator that wraps the pulisher with a blocking interface.
     */
    public static <T> Iterator<T> toIteratorWithRequestsPipelineSize(final Publisher<T> publisher,
                                                                     final int numRequestsPipeline) {
        return new StreamToIterator<>(publisher, numRequestsPipeline);
    }

    /**
     * Returns {@code true} if the iteration has more elements.
     * (In other words, returns {@code true} if {@link #next} would
     * return an element rather than throwing an exception.)
     * <p>
     * Checks to see if the stream is closed or has sent an error.
     *
     * @return {@code true} if the iteration has more elements
     */
    @Override
    public boolean hasNext() {

        /* If we are in the completed state let the end user know. */
        if (complete) {
            return false;
        }

        /* If we called hasNext twice, do not poll the queue. */
        if (next != null) {
            return true;
        }

        /* Get the item from the queue, and block until we do. */
        Object item = null;

        while (item == null) {
            try {
                item = blockingQueue.take();
            } catch (InterruptedException e) {
                Thread.interrupted();
            }
        }

        /* Check to see if the complete message was sent.
        * If so mark this iterator has completed and set the next item to null.
        */
        if (item == COMPLETE) {
            complete = true;
            next = null;
            return false;
        } else {
            /* Store the next item. */
            next = (T) item;
            return true;
        }

    }

    /**
     * Returns the next element in the iteration.
     * This will throw a {@code StreamException} wrapping the error sent from the stream.
     *
     * @return the next element in the iteration
     * @throws NoSuchElementException if the iteration has no more elements
     */
    @Override
    public T next() {

        /* If the item is null and we are complete then calling this is a programming error. */
        if (next == null && complete) {
            throw new NoSuchElementException("The stream has completed. There are no more items.");
        }

        /* If the item is an error, throw the error wrapped by a runtime error. */
        if (next == ERROR) {
            try {
                final Object error = blockingQueue.take();
                complete = true;
                throw new StreamException("Exception from stream", (Throwable) error);
            } catch (InterruptedException e) {
                Thread.interrupted();
                throw new StreamException("Stream sent error but unable to get error from stream", e);
            }
        } else {
            /* Give them the next item but don't store the next item any longer. */
            T theNext = next;
            next = null;
            return theNext;
        }
    }

    /**
     * Used to wrap the original exception from the stream so we can rethrow it.
     */
    public static class StreamException extends RuntimeException {
        public StreamException(String reason, Throwable throwable) {
            super(reason, throwable);
        }
    }
}


/*
 * 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.http.server.impl;

import io.advantageous.qbit.GlobalConstants;
import io.advantageous.qbit.concurrent.ExecutorContext;
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.request.impl.HttpResponseCreatorDefault;
import io.advantageous.qbit.http.server.HttpServer;
import io.advantageous.qbit.http.server.RequestContinuePredicate;
import io.advantageous.qbit.http.server.websocket.WebSocketMessage;
import io.advantageous.qbit.http.websocket.WebSocket;
import io.advantageous.qbit.http.websocket.WebSocketSender;
import io.advantageous.qbit.service.ServiceProxyUtils;
import io.advantageous.qbit.service.discovery.EndpointDefinition;
import io.advantageous.qbit.service.discovery.ServiceDiscovery;
import io.advantageous.qbit.service.health.HealthServiceAsync;
import io.advantageous.qbit.service.health.HealthStatus;
import io.advantageous.qbit.system.QBitSystemManager;
import io.advantageous.qbit.util.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Predicate;

import static io.advantageous.qbit.concurrent.ScheduledExecutorBuilder.scheduledExecutorBuilder;
import static io.advantageous.qbit.http.server.websocket.WebSocketMessageBuilder.webSocketMessageBuilder;

/**
 * Captures all of the logic of an HTTP server that QBit implements, but does not
 * handle the actual wire transfer.
 *
 * @author rhightower on 2/12/15.
 */
public class SimpleHttpServer implements HttpServer {
    final AtomicLong lastCheckIn = new AtomicLong(Timer.clockTime());
    final AtomicBoolean ok = new AtomicBoolean(true);
    private final Logger logger = LoggerFactory.getLogger(SimpleHttpServer.class);
    private final boolean debug = GlobalConstants.DEBUG || logger.isDebugEnabled();
    private final QBitSystemManager systemManager;
    private final int flushInterval;
    private final ServiceDiscovery serviceDiscovery;
    private final HealthServiceAsync healthServiceAsync;
    private final String name;
    private final int port;
    private final String host;
    private final long checkInEveryMiliDuration;
    private final CopyOnWriteArrayList<HttpResponseDecorator> decorators;
    private final HttpResponseCreator httpResponseCreator;
    private final EndpointDefinition endpointDefinition;
    protected Runnable onStart;
    protected Consumer<Throwable> errorHandler;
    private Consumer<WebSocketMessage> webSocketMessageConsumer = webSocketMessage -> {
    };
    private Consumer<WebSocketMessage> webSocketCloseMessageConsumer = webSocketMessage -> {
    };
    private Consumer<HttpRequest> httpRequestConsumer = request -> {
    };
    private Consumer<Void> requestIdleConsumer = aVoid -> {
    };
    private Consumer<WebSocket> webSocketConsumer = this::defaultWebSocketHandler;
    private Consumer<Void> webSocketIdleConsumer = aVoid -> {
    };
    private Predicate<HttpRequest> shouldContinueHttpRequest = request -> true;
    private Predicate<HttpRequest> shouldContinueReadingRequestBody = request -> true;
    private ExecutorContext executorContext;
    private Predicate<WebSocket> shouldContinueWebSocket = webSocket -> true;

    public SimpleHttpServer(
            final String endpointName,
            final QBitSystemManager systemManager,
            final int flushInterval,
            final String host,
            final int port,
            final ServiceDiscovery serviceDiscovery,
            final HealthServiceAsync healthServiceAsync,
            final int serviceDiscoveryTtl,
            final TimeUnit serviceDiscoveryTtlTimeUnit,
            final CopyOnWriteArrayList<HttpResponseDecorator> decorators,
            final HttpResponseCreator httpResponseCreator,
            final RequestContinuePredicate requestBodyContinuePredicate) {
        this.decorators = decorators;
        this.httpResponseCreator = httpResponseCreator;

        this.shouldContinueReadingRequestBody = requestBodyContinuePredicate;

        this.name = endpointName == null ? "HTTP_SERVER_" + port : endpointName;
        this.port = port;
        this.host = host;
        this.systemManager = systemManager;
        this.flushInterval = flushInterval;
        this.serviceDiscovery = serviceDiscovery;
        this.healthServiceAsync = healthServiceAsync;
        this.endpointDefinition = createEndpointDefinition(serviceDiscoveryTtl,
                serviceDiscoveryTtlTimeUnit);

        this.checkInEveryMiliDuration =
                serviceDiscoveryTtlTimeUnit.toMillis(serviceDiscoveryTtl) / 3;
    }

    public SimpleHttpServer() {

        this.port = 8080;
        this.host = "localhost";
        this.name = "HTTP_SERVER";
        this.systemManager = null;
        this.flushInterval = 1;
        this.serviceDiscovery = null;
        this.healthServiceAsync = null;
        this.endpointDefinition = null;
        this.checkInEveryMiliDuration = 100_000;
        this.decorators = new CopyOnWriteArrayList<>();
        this.httpResponseCreator = new HttpResponseCreatorDefault();
    }

    public Runnable getOnStart() {
        if (onStart == null) {
            onStart = () -> {
            };
        }
        return onStart;
    }

    @Override
    public void setOnStart(final Runnable runnable) {
        this.onStart = runnable;
    }

    public Consumer<Throwable> getErrorHandler() {
        if (errorHandler == null) {
            errorHandler = Throwable::printStackTrace;
        }
        return errorHandler;
    }

    EndpointDefinition createEndpointDefinition(int serviceDiscoveryTtl, TimeUnit serviceDiscoveryTtlTimeUnit) {
        EndpointDefinition endpointDefinition;
        if (serviceDiscovery != null) {
            endpointDefinition = serviceDiscovery.registerWithTTL(name, host, port,
                    (int) serviceDiscoveryTtlTimeUnit.toSeconds(serviceDiscoveryTtl));
            serviceDiscovery.checkInOk(endpointDefinition.getId());
        } else {
            endpointDefinition = null;
        }
        return endpointDefinition;
    }

    @Override
    public void setOnError(final Consumer<Throwable> exceptionConsumer) {
        this.errorHandler = exceptionConsumer;
    }

    /**
     * Main entry point.
     *
     * @param request request to handle
     */
    public void handleRequest(final HttpRequest request) {
        if (debug) {
            System.out.println("HttpServer::handleRequest " + request);
            logger.debug("HttpServer::handleRequest" + request);
        }
        if (shouldContinueHttpRequest.test(request)) {
            httpRequestConsumer.accept(request);
        }
    }

    public void handleWebSocketMessage(final WebSocketMessage webSocketMessage) {
        webSocketMessageConsumer.accept(webSocketMessage);
    }

    public void handleWebSocketClosedMessage(WebSocketMessage webSocketMessage) {
        webSocketCloseMessageConsumer.accept(webSocketMessage);
    }

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

    public void setShouldContinueWebSocket(Predicate<WebSocket> predicate) {
        this.shouldContinueWebSocket = predicate;
    }

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

    @Override
    public void setWebSocketCloseConsumer(Consumer<WebSocketMessage> webSocketCloseMessageConsumer) {
        this.webSocketCloseMessageConsumer = webSocketCloseMessageConsumer;
    }

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

    @Override
    public void setHttpRequestsIdleConsumer(Consumer<Void> idleConsumer) {
        this.requestIdleConsumer = idleConsumer;
    }

    @Override
    public void setWebSocketIdleConsume(Consumer<Void> idleConsumer) {
        this.webSocketIdleConsumer = idleConsumer;
    }

    @Override
    public void start() {

        if (debug) {
            logger.debug("HttpServer Started");
        }

        startPeriodicFlush();

    }

    private void startPeriodicFlush() {
        if (executorContext != null) {
            throw new IllegalStateException("Can't call startClient twice");
        }

        executorContext = scheduledExecutorBuilder()
                .setThreadName("HttpServer")
                .setInitialDelay(flushInterval)
                .setPeriod(flushInterval)
                .setDescription("HttpServer Periodic Flush")
                .setRunnable(() -> {
                    handleRequestQueueIdle();
                    handleWebSocketQueueIdle();
                })
                .build();

        executorContext.start();
    }

    @Override
    public void stop() {

        if (systemManager != null) systemManager.serviceShutDown();

        if (debug) {
            logger.debug("HttpServer Stopped");
        }

        if (executorContext != null) {
            executorContext.stop();
        }

    }

    public void handleWebSocketQueueIdle() {

        webSocketIdleConsumer.accept(null);
    }

    public void handleRequestQueueIdle() {

        if (serviceDiscovery != null) {
            handleCheckIn();
        }
        requestIdleConsumer.accept(null);
    }

    public void handleCheckIn() {

        if (healthServiceAsync == null) {
            if (Timer.clockTime() - lastCheckIn.get() > checkInEveryMiliDuration) {
                lastCheckIn.set(Timer.clockTime());
                serviceDiscovery.checkInOk(endpointDefinition.getId());
            }
        } else {

            if (Timer.clockTime() - lastCheckIn.get() > checkInEveryMiliDuration) {
                lastCheckIn.set(Timer.clockTime());

                healthServiceAsync.ok(ok::set);
                ServiceProxyUtils.flushServiceProxy(healthServiceAsync);

                if (ok.get()) {
                    serviceDiscovery.checkInOk(endpointDefinition.getId());
                } else {
                    serviceDiscovery.checkIn(endpointDefinition.getId(), HealthStatus.FAIL);
                }

                ServiceProxyUtils.flushServiceProxy(serviceDiscovery);

            }
        }
    }

    public void handleOpenWebSocket(final WebSocket webSocket) {
        if (this.shouldContinueWebSocket.test(webSocket)) {
            this.webSocketConsumer.accept(webSocket);
        }
    }

    private void defaultWebSocketHandler(final WebSocket webSocket) {

        webSocket.setTextMessageConsumer(webSocketMessageIn -> {

            final WebSocketMessage webSocketMessage = webSocketMessageBuilder()
                    .setMessage(webSocketMessageIn)
                    .setUri(webSocket.uri())
                    .setRemoteAddress(webSocket.remoteAddress())
                    .setTimestamp(Timer.timer().now()).setSender(
                            message -> {
                                if (webSocket.isOpen()) {
                                    webSocket.sendText(message);
                                }

                            }).build();
            handleWebSocketMessage(webSocketMessage);

        });

        webSocket.setBinaryMessageConsumer(webSocketMessageIn -> {

            final WebSocketMessage webSocketMessage = webSocketMessageBuilder()
                    .setMessage(webSocketMessageIn)
                    .setUri(webSocket.uri())
                    .setRemoteAddress(webSocket.remoteAddress())
                    .setTimestamp(Timer.timer().now()).setSender(
                            new WebSocketSender() {
                                @Override
                                public void sendText(String message) {
                                    webSocket.sendBinary(message.getBytes(StandardCharsets.UTF_8));
                                }

                                @Override
                                public void sendBytes(byte[] message) {
                                    webSocket.sendBinary(message);
                                }
                            }

                    ).build();
            handleWebSocketMessage(webSocketMessage);

        });

        webSocket.setCloseConsumer(aVoid -> {

            long time = Timer.timer().now();

            final WebSocketMessage webSocketMessage = webSocketMessageBuilder()

                    .setUri(webSocket.uri())
                    .setRemoteAddress(webSocket.remoteAddress())
                    .setTimestamp(time).build();

            handleWebSocketClosedMessage(webSocketMessage);

        });

        webSocket.setErrorConsumer(e -> logger.error("Error with WebSocket handling", e));

    }

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

    public CopyOnWriteArrayList<HttpResponseDecorator> getDecorators() {
        return decorators;
    }

    public HttpResponseCreator getHttpResponseCreator() {
        return httpResponseCreator;
    }

    public Predicate<HttpRequest> getShouldContinueReadingRequestBody() {
        return shouldContinueReadingRequestBody;
    }

    public SimpleHttpServer setShouldContinueReadingRequestBody(Predicate<HttpRequest> shouldContinueReadingRequestBody) {
        this.shouldContinueReadingRequestBody = shouldContinueReadingRequestBody;
        return this;
    }
}


package io.advantageous.qbit.events;

import io.advantageous.qbit.queue.Queue;
import io.advantageous.qbit.queue.ReceiveQueue;
import io.advantageous.qbit.service.Startable;
import io.advantageous.qbit.service.Stoppable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

/**
 * Sends an item from a QBit queue to the event bus.
 * This can be started or the process method could be called periodically.
 * It can be used to channel events from Kafka, JMS, and/or Redis into the QBit world.
 */
public class EventBusQueueAdapter<T> implements Startable, Stoppable {

    /**
     * Event Manager.
     */
    private final EventManager eventManager;
    /**
     * Channel to send messages to from the Queue.
     */
    private final String channel;
    /**
     * Lock.
     */
    private final Lock lock = new ReentrantLock();
    /**
     * Logger.
     */
    private final Logger logger = LoggerFactory.getLogger(EventBusQueueAdapter.class);
    /**
     * Debug is on or off.
     */
    private final boolean debug = logger.isDebugEnabled();
    private final Supplier<Queue<T>> queueSupplier;
    /**
     * Queue.
     */
    private Optional<Queue<T>> queue = Optional.empty();
    private Optional<ReceiveQueue<T>> receiveQueue = Optional.empty();

    /**
     * @param queueSupplier queueSupplier
     * @param eventManager  event manager
     * @param channel       channel
     */
    public EventBusQueueAdapter(
            final Supplier<Queue<T>> queueSupplier,
            final EventManager eventManager,
            final String channel) {

        this.queueSupplier = queueSupplier;
        initQueue();
        this.eventManager = eventManager;
        this.channel = channel;
    }

    private void initQueue() {

        /* Clean it up. */
        queue.ifPresent(actualQueue -> {

            try {
                actualQueue.stop();
            } catch (Exception ex) {
                logger.debug("Unable to stop queue", ex);
            }
        });

        receiveQueue.ifPresent(actualReceiveQueue -> {
            try {
                actualReceiveQueue.stop();
            } catch (Exception ex) {
                logger.debug("Unable to shut down receive queue", ex);
            }
        });

        try {
            this.queue = Optional.of(queueSupplier.get());
            this.queue.ifPresent(actualQueue -> receiveQueue = Optional.of(actualQueue.receiveQueue()));
        } catch (Exception ex) {
            logger.error("Unable to create queue with queue supplier", ex);
            this.queue = Optional.empty();
            this.receiveQueue = Optional.empty();
        }
    }

    /**
     * Process. This can be called periodically and it will check to see if there are messages on the queue.
     */
    public void process() {

        if (!receiveQueue.isPresent()) {
            initQueue();
        }

        receiveQueue.ifPresent(receiveQueue -> {

            T item;

            do {
                try {
                    item = receiveQueue.poll();
                } catch (Exception ex) {
                    logger.debug("Unable to receive message", ex);
                    initQueue();
                    item = null;
                }
                if (item != null) {
                    sendToEventManager(item);
                }
            } while (item != null);
        });

    }

    /**
     * Send the queue item to the event.
     *
     * @param item item
     */
    private void sendToEventManager(T item) {
        if (debug) {
            logger.debug("EventBusQueueAdapter::sendToEventManager({})", item);
        }
        try {
            if (lock.tryLock()) {
                eventManager.sendArguments(channel, item);
            } else {
                lock.lock();
                eventManager.sendArguments(channel, item);
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Start listener. Once this is called messages can come in on a foreign thread.
     */
    public void start() {
        if (!queue.isPresent()) {
            initQueue();
        }
        queue.ifPresent(actualQueue -> actualQueue.startListener(EventBusQueueAdapter.this::sendToEventManager));
    }

    /**
     * Stop the adapter.
     */
    public void stop() {
        queue.ifPresent(actualQueue -> actualQueue.stop());
    }
}