/*
 * 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.boon.client;

import io.advantageous.boon.core.*;
import io.advantageous.boon.core.reflection.ClassMeta;
import io.advantageous.boon.core.reflection.MapObjectConversion;
import io.advantageous.boon.core.reflection.MethodAccess;
import io.advantageous.boon.primitive.Arry;
import io.advantageous.qbit.QBit;
import io.advantageous.qbit.client.BeforeMethodSent;
import io.advantageous.qbit.client.Client;
import io.advantageous.qbit.client.ClientProxy;
import io.advantageous.qbit.http.client.HttpClient;
import io.advantageous.qbit.http.websocket.WebSocket;
import io.advantageous.qbit.message.Message;
import io.advantageous.qbit.message.MethodCall;
import io.advantageous.qbit.message.Response;
import io.advantageous.qbit.message.impl.MethodCallImpl;
import io.advantageous.qbit.reactive.Callback;
import io.advantageous.qbit.sender.Sender;
import io.advantageous.qbit.service.BeforeMethodCall;
import io.advantageous.reakt.promise.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.ref.WeakReference;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.ConnectException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

import static io.advantageous.boon.core.Exceptions.die;
import static io.advantageous.boon.core.Str.sputs;
import static io.advantageous.qbit.service.Protocol.PROTOCOL_ARG_SEPARATOR;

/**
 * Factory to createWithWorkers client proxies using interfaces.
 * created by Richard on 10/2/14.
 *
 * @author Rick Hightower
 */
public class BoonClient implements Client {

    private final String uri;
    /**
     * The server we are calling.
     */
    private final HttpClient httpServerProxy;
    /**
     * Logger.
     */
    private final Logger logger = LoggerFactory.getLogger(BoonClient.class);

    /**
     * Request batch size for queuing.
     */
    private final int requestBatchSize;
    private final boolean debug = logger.isDebugEnabled();
    private final BeforeMethodSent beforeMethodSent;
    /**
     * Map of handlers so we can do the whole async call back thing.
     */
    private final Map<HandlerKey, Callback<Object>> handlers = new ConcurrentHashMap<>();
    /**
     * List of client proxies that we are managing for periodic flush.
     */
    private final List<ClientProxy> clientProxies = new CopyOnWriteArrayList<>();
    private final AtomicBoolean connected = new AtomicBoolean();
    /**
     * Holds on to Boon cache so we don't have to recreate reflected gak.
     */
    Object context = Sys.contextToHold();
    private WebSocket webSocket;

    /**
     * @param httpClient       httpClient
     * @param uri              uri
     * @param requestBatchSize request batch size
     * @param beforeMethodSent before method sent interceptor
     */
    public BoonClient(final String uri,
                      final HttpClient httpClient,
                      final int requestBatchSize,
                      final BeforeMethodSent beforeMethodSent) {

        this.httpServerProxy = httpClient;
        this.uri = uri;
        this.requestBatchSize = requestBatchSize;
        this.beforeMethodSent = beforeMethodSent;
    }

    /**
     * Stop client. Stops processing call backs.
     */
    public void stop() {
        flush();
        if (httpServerProxy != null) {
            try {
                httpServerProxy.stop();
            } catch (Exception ex) {

                logger.warn("Problem closing httpServerProxy ", ex);
            }
        }
    }

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

    /**
     * Handles WebSocket messages and parses them into responses.
     * This does not handle batching or rather un-batching which we need for performance
     * we do handle batching in the parser/encoder.
     *
     * @param webSocketText websocket text
     */
    private void handleWebSocketReplyMessage(final String webSocketText) {

        final List<Message<Object>> messages = QBit.factory().createProtocolParser().parse("", webSocketText);

        //noinspection Convert2streamapi
        for (Message<Object> message : messages) {
            if (message instanceof Response) {
                @SuppressWarnings("unchecked") final Response<Object> response = ((Response) message);
                final String[] split = StringScanner.split(response.returnAddress(), (char) PROTOCOL_ARG_SEPARATOR);
                final HandlerKey key = split.length == 2 ? new HandlerKey(split[1], response.id()) : new HandlerKey(split[0], response.id());
                final Callback<Object> handler = handlers.get(key);

                if (handler != null) {
                    handleAsyncCallback(response, handler);
                    handlers.remove(key);
                } // else there was no handler, it was a one way method.
            }
        }
    }

    /**
     * Handles an async callbackWithTimeout.
     */
    private void handleAsyncCallback(final Response<Object> response, final Callback<Object> handler) {
        if (response.wasErrors()) {
            handler.onError(new Exception(response.body().toString()));
        } else {
            handler.accept(response.body());
        }
    }

    /**
     * Flush the calls and flush the proxy.
     */
    public void flush() {
        //noinspection Convert2streamapi
        for (ClientProxy clientProxy : clientProxies) {
            clientProxy.clientProxyFlush();
        }
        httpServerProxy.flush();
    }

    /**
     * Sends a message over websocket.
     *
     * @param message     message to sendText over WebSocket
     * @param serviceName message to sendText over WebSocket
     */
    private void send(final String serviceName, final String message, final Consumer<Exception> exceptionConsumer) {

        if (webSocket == null) {
            String webSocketURI;
            if (serviceName.startsWith(uri)) {
                webSocketURI = serviceName;
            } else {
                webSocketURI = Str.add(uri, "/", serviceName);
            }
            this.webSocket = httpServerProxy.createWebSocket(webSocketURI);
            wireWebSocket(serviceName, message);
        }

        try {
            if (webSocket.isClosed() && connected()) {
                this.webSocket.openAndNotify(netSocket -> {
                    connected.set(true);
                    webSocket.sendText(message, exceptionConsumer);
                }, exceptionConsumer);
            } else {
                webSocket.sendText(message);
            }
        } catch (Exception ex) {
            this.connected.set(false);
            if (debug) throw ex;
        }
    }

    private void wireWebSocket(final String serviceName, final String message) {

        this.webSocket.setErrorConsumer(error -> {

            if (error instanceof ConnectException) {
                connected.set(false);
            }
            logger.error(sputs(this.getClass().getName(),
                    "::Exception calling WebSocket from client proxy",
                    "\nService Name", serviceName, "\nMessage", message), error);
        });

        //noinspection Convert2MethodRef
        this.webSocket.setTextMessageConsumer(messageFromServer -> handleWebSocketReplyMessage(messageFromServer));
    }

    /**
     * Creates a new client proxy given a client interface.
     *
     * @param serviceInterface client interface
     * @param serviceName      client name
     * @param <T>              class type of interface
     * @return new client proxy.. calling methods on this proxy marshals method calls to httpServerProxy.
     */
    public <T> T createProxy(final Class<T> serviceInterface, final String serviceName) {

        return createProxy(serviceInterface, serviceName, Str.join('-', uri, serviceName, UUID.randomUUID().toString()));
    }

    /**
     * @param serviceInterface client interface
     * @param serviceName      client name
     * @param returnAddressArg specify a specific return address
     * @param <T>              class type of client interface
     * @return proxy object
     */
    public <T> T createProxy(final Class<T> serviceInterface, final String serviceName, final String returnAddressArg) {

        if (!serviceInterface.isInterface()) {
            die("QBitClient:: The service interface must be an interface");
        }

        /** Use this before call to register an async handler with the handlers map. */
        BeforeMethodCall beforeMethodCall = call -> {
            registerCallback(serviceInterface, call);
            prepareBody(call);
            return true;
        };

        final Sender<String> sender = new Sender<String>() {

            @Override
            public void send(String returnAddress, String buffer, Consumer<Exception> exceptionConsumer) {
                BoonClient.this.send(serviceName, buffer, exceptionConsumer);
            }

            @Override
            public void stop() {
                BoonClient.this.stop();
            }
        };

        T proxy = QBit.factory().createRemoteProxyWithReturnAddress(serviceInterface, uri, serviceName,
                httpServerProxy.getHost(),
                httpServerProxy.getPort(),
                connected,
                returnAddressArg, sender, beforeMethodCall, requestBatchSize, beforeMethodSent);

        if (proxy instanceof ClientProxy) {
            clientProxies.add((ClientProxy) proxy);
        }

        return proxy;
    }

    private void prepareBody(MethodCall call) {
        final Object body = call.body();
        if (body instanceof Object[]) {

            Object[] list = (Object[]) body;

            if (list.length > 0) {
                final Object o = list[0];
                if (o instanceof Callback) {
                    //noinspection unchecked
                    if (list.length - 1 == 0) {
                        list = new Object[0];
                    } else {
                        list = Arry.slc(list, 1); //Skip first arg it was a handler.
                    }
                }
                if (call instanceof MethodCallImpl) {
                    MethodCallImpl impl = (MethodCallImpl) call;
                    impl.setBody(list);
                }

            }
        }
    }

    private <T> void registerCallback(Class<T> serviceInterface, MethodCall call) {
        final Callback callback = call.callback();

        if (callback != null) {
            handlers.put(new HandlerKey(call.returnAddress(), call.id()),
                    createHandler(serviceInterface, call, callback));
        }
    }

    /**
     * Create an async handler. Uses some generics reflection to see what the actual type is
     *
     * @param serviceInterface client interface
     * @param call             method call object
     * @param handler          handler that will handle the message
     * @param <T>              the class of hte client interface
     * @return the new handler
     */
    private <T> Callback createHandler(final Class<T> serviceInterface, final MethodCall call,
                                       final Callback handler) {

        final ClassMeta<T> clsMeta = ClassMeta.classMeta(serviceInterface);
        final MethodAccess method = clsMeta.method(call.name());

        Class<?> returnType = null;
        Class<?> compType = null;

        if (Promise.class.isAssignableFrom(method.returnType()) ) {

            Type t0 = method.method().getGenericReturnType();
            if (t0 instanceof ParameterizedType) {
                ParameterizedType parameterizedType = ((ParameterizedType) t0);
                Type type = ((parameterizedType != null ? parameterizedType.getActualTypeArguments().length : 0) > 0 ? (parameterizedType != null ? parameterizedType.getActualTypeArguments() : new Type[0])[0] : null);

                if (type instanceof ParameterizedType) {
                    returnType = (Class) ((ParameterizedType) type).getRawType();
                    final Type type1 = ((ParameterizedType) type).getActualTypeArguments()[0];

                    if (type1 instanceof Class) {
                        compType = (Class) type1;
                    }
                } else if (type instanceof Class) {
                    returnType = (Class<?>) type;
                }

            }
        } else {
            if (method.parameterTypes().length > 0) {
                Type[] genericParameterTypes = method.getGenericParameterTypes();
                ParameterizedType parameterizedType = genericParameterTypes.length > 0 ? (ParameterizedType) genericParameterTypes[0] : null;

                Type type = ((parameterizedType != null ? parameterizedType.getActualTypeArguments().length : 0) > 0 ? (parameterizedType != null ? parameterizedType.getActualTypeArguments() : new Type[0])[0] : null);

                if (type instanceof ParameterizedType) {
                    returnType = (Class) ((ParameterizedType) type).getRawType();
                    final Type type1 = ((ParameterizedType) type).getActualTypeArguments()[0];

                    if (type1 instanceof Class) {
                        compType = (Class) type1;
                    }
                } else if (type instanceof Class) {
                    returnType = (Class<?>) type;
                }

            }
        }
        final Class<?> actualReturnType = returnType;

        final Class<?> componentClass = compType;

        /** Create the return handler. */
        return new Callback<Object>() {
            @Override
            public void accept(Object event) {

                if (actualReturnType != null) {

                    if (componentClass != null && actualReturnType == List.class) {

                        try {
                            //noinspection unchecked
                            event = MapObjectConversion.convertListOfMapsToObjects(componentClass, (List) event);
                        } catch (Exception ex) {
                            if (event instanceof CharSequence) {
                                String errorMessage = event.toString();
                                if (errorMessage.startsWith("java.lang.IllegalState")) {
                                    handler.onError(new IllegalStateException(errorMessage));
                                    return;
                                } else {
                                    handler.onError(new IllegalStateException("Conversion error"));
                                    return;
                                }
                            } else {
                                handler.onError(new IllegalStateException("Conversion error"));
                                return;
                            }
                        }
                    } else {

                        switch (actualReturnType.getName()) {
                            case "java.lang.Boolean":
                                if (event instanceof Value) {
                                    event = ((Value) event).booleanValue();
                                } else {
                                    event = Conversions.coerce(actualReturnType, event);
                                }
                                break;
                            default:
                                event = Conversions.coerce(actualReturnType, event);
                        }
                    }
                    //noinspection unchecked
                    handler.accept(event);
                }

            }

            @Override
            public void onError(Throwable error) {
                handler.onError(error);
            }

            @Override
            public void onTimeout() {
                handler.onTimeout();
            }
        };

    }

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

    public void startWithNotify(final Runnable runnable) {

        /** Adding the weak reference to get rid of the circular dependency
         * which seems to prevent this from getting collected.
         */
        final WeakReference<BoonClient> boonClientWeakReference =
                new WeakReference<>(this);
        this.httpServerProxy.periodicFlushCallback(aVoid -> {
            final BoonClient boonClient = boonClientWeakReference.get();
            if (boonClient != null) {
                boonClient.flush();
            }
        });

        if (runnable == null) {
            this.httpServerProxy.startClient();
        } else {
            this.httpServerProxy.startWithNotify(runnable);
        }
        connected.set(true);

    }

    /**
     * Key to store callbackWithTimeout in call back map.
     */
    private class HandlerKey {
        /**
         * Return address
         */
        final String returnAddress;
        /**
         * Message id
         */
        final long messageId;

        private HandlerKey(String returnAddress, long messageId) {
            this.returnAddress = returnAddress;
            this.messageId = messageId;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            HandlerKey that = (HandlerKey) o;
            return messageId == that.messageId && !(returnAddress != null ? !returnAddress.equals(that.returnAddress) : that.returnAddress != null);
        }

        @Override
        public int hashCode() {
            int result = returnAddress != null ? returnAddress.hashCode() : 0;
            result = 31 * result + (int) (messageId ^ (messageId >>> 32));
            return result;
        }
    }
}


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

import io.advantageous.qbit.GlobalConstants;
import io.advantageous.qbit.http.HttpTransport;
import io.advantageous.qbit.http.request.HttpRequest;
import io.advantageous.qbit.http.server.HttpServer;
import io.advantageous.qbit.http.server.websocket.WebSocketMessage;
import io.advantageous.qbit.json.JsonMapper;
import io.advantageous.qbit.message.Request;
import io.advantageous.qbit.message.Response;
import io.advantageous.qbit.queue.QueueCallBackHandler;
import io.advantageous.qbit.queue.ReceiveQueueListener;
import io.advantageous.qbit.service.ServiceBundle;
import io.advantageous.qbit.service.ServiceProxyUtils;
import io.advantageous.qbit.service.ServiceQueue;
import io.advantageous.qbit.service.Stoppable;
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.spi.ProtocolEncoder;
import io.advantageous.qbit.spi.ProtocolParser;
import io.advantageous.qbit.system.QBitSystemManager;
import io.advantageous.qbit.util.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;

/**
 * Implementation of a service endpoint server.  This is the server that exposes a set of qbit services to the network.
 *
 * @author richardhightower@gmail.com (Rick Hightower)
 * @author gcc@rd.io (Geoff Chandler)
 */
public class ServiceEndpointServerImpl implements ServiceEndpointServer {
    protected final WebSocketServiceServerHandler webSocketHandler;
    protected final HttpRequestServiceServerHandler httpRequestServerHandler;
    protected final ProtocolEncoder encoder;
    protected final HttpTransport httpServer;
    protected final ServiceBundle serviceBundle;
    protected final JsonMapper jsonMapper;
    protected final ProtocolParser parser;
    protected final Consumer<Throwable> errorHandler;
    private final Logger logger = LoggerFactory.getLogger(ServiceEndpointServerImpl.class);
    private final boolean debug = GlobalConstants.DEBUG || logger.isDebugEnabled();
    private final QBitSystemManager systemManager;
    private final EndpointDefinition endpoint;
    private final HealthServiceAsync healthServiceAsync;
    private final AtomicBoolean stop = new AtomicBoolean();
    /* Used for service discovery and registration. */
    private final ServiceDiscovery serviceDiscovery;
    protected int timeoutInSeconds = 30;

    public ServiceEndpointServerImpl(final HttpTransport httpServer, final ProtocolEncoder encoder,
                                     final ProtocolParser parser,
                                     final ServiceBundle serviceBundle,
                                     final JsonMapper jsonMapper,
                                     final int timeOutInSeconds,
                                     final int numberOfOutstandingRequests,
                                     final int protocolBatchSize,
                                     final int flushInterval,
                                     final QBitSystemManager systemManager,
                                     final String endpointName,
                                     final String endpointId,
                                     final List<String> endpointTags,
                                     final ServiceDiscovery serviceDiscovery,
                                     final String host,
                                     final int port,
                                     final int ttlSeconds,
                                     final HealthServiceAsync healthServiceAsync,
                                     final Consumer<Throwable> errorHandler,
                                     final long flushResponseInterval,
                                     final int parserWorkerCount,
                                     final int encoderWorkerCount) {

        this.systemManager = systemManager;
        this.encoder = encoder;
        this.parser = parser;
        this.httpServer = httpServer;
        this.serviceBundle = serviceBundle;
        this.jsonMapper = jsonMapper;
        this.timeoutInSeconds = timeOutInSeconds;

        this.errorHandler = errorHandler;

        this.healthServiceAsync = healthServiceAsync;

        this.webSocketHandler = new WebSocketServiceServerHandler(protocolBatchSize, serviceBundle,
                parserWorkerCount, encoderWorkerCount, flushResponseInterval);

        this.serviceDiscovery = serviceDiscovery;

        httpRequestServerHandler =
                new HttpRequestServiceServerHandlerUsingMetaImpl(this.timeoutInSeconds,
                        serviceBundle, jsonMapper, numberOfOutstandingRequests, flushInterval, errorHandler);

        this.endpoint = createEndpoint(endpointName, endpointId, endpointTags, host, port, ttlSeconds);

    }

    private EndpointDefinition createEndpoint(String endpointName, String endpointId, List<String> endpointTags, String host, int port, int
            ttlSeconds) {
        if (serviceDiscovery != null) {
            if (ttlSeconds > 0) {
                if (endpointId != null) {
                    if (endpointTags != null) {
                        return serviceDiscovery.registerWithIdAndTTLAndTags(endpointName, endpointId, host, port, ttlSeconds, endpointTags);
                    }
                    return serviceDiscovery.registerWithIdAndTimeToLive(endpointName, endpointId, host, port, ttlSeconds);
                }
                return serviceDiscovery.registerWithTTL(endpointName, host, port, ttlSeconds);
            } else {
                return serviceDiscovery.register(endpointName, host, port);
            }
        }
        return null;
    }

    @Override
    public void start() {
        doStart();
        httpServer.start();
    }

    @Override
    public void startWithNotify(Runnable runnable) {
        doStart();
        httpServer.startWithNotify(runnable);
    }

    @Override
    public ServiceEndpointServer startServerAndWait() {
        doStart();
        if (httpServer instanceof HttpServer) {
            ((HttpServer) httpServer).startServerAndWait();
        } else {
            httpServer.start();
        }
        return this;
    }

    private void doStart() {
        stop.set(false);

        httpRequestServerHandler.start();

        httpServer.setHttpRequestConsumer(httpRequestServerHandler::handleRestCall);
        httpServer.setWebSocketMessageConsumer(webSocketHandler::handleWebSocketCall);
        httpServer.setWebSocketCloseConsumer(webSocketHandler::handleWebSocketClose);

        if (endpoint != null && endpoint.getTimeToLive() > 0) {
            handleServiceDiscoveryCheckIn();
        } else {
            httpServer.setHttpRequestsIdleConsumer(httpRequestServerHandler::httpRequestQueueIdle);
        }

        httpServer.setWebSocketIdleConsume(webSocketHandler::webSocketQueueIdle);

        serviceBundle.startUpCallQueue();
        startResponseQueueListener();
    }

    private void handleServiceDiscoveryCheckIn() {
        final AtomicLong lastCheckIn = new AtomicLong(Timer.clockTime());

        final long checkinDuration = endpoint.getTimeToLive() * 1000 / 2;

        if (healthServiceAsync == null) {
            handleDiscoveryCheckInNoHealth(lastCheckIn, checkinDuration);
        } else {
            handleDiscoveryCheckInWithHealth(lastCheckIn, checkinDuration);
        }
    }

    private void handleDiscoveryCheckInNoHealth(final AtomicLong lastCheckIn,
                                                final long checkInDuration) {
        httpServer.setHttpRequestsIdleConsumer(aVoid -> {
            httpRequestServerHandler.httpRequestQueueIdle(null);

            long now = Timer.clockTime();

            if (now > lastCheckIn.get() + checkInDuration) {
                lastCheckIn.set(now);
                serviceDiscovery.checkInOk(endpoint.getId());
            }

        });
    }

    private void handleDiscoveryCheckInWithHealth(final AtomicLong lastCheckIn,
                                                  final long checkInDuration) {
        final AtomicBoolean ok = new AtomicBoolean(true);

        httpServer.setHttpRequestsIdleConsumer(aVoid -> {
            httpRequestServerHandler.httpRequestQueueIdle(null);

            long now = Timer.clockTime();

            if (now > lastCheckIn.get() + checkInDuration) {
                lastCheckIn.set(now);
                if (ok.get()) {
                    serviceDiscovery.checkInOk(endpoint.getId());
                } else {
                    serviceDiscovery.checkIn(endpoint.getId(), HealthStatus.FAIL);
                }
                healthServiceAsync.ok(ok::set);
                ServiceProxyUtils.flushServiceProxy(healthServiceAsync);
            }
        });
    }

    public void stop() {

        try {
            serviceBundle.stop();
        } catch (Exception ex) {
            if (debug) logger.debug("Unable to cleanly shutdown bundle", ex);
        }

        try {
            if (httpServer instanceof Stoppable) {
                ((Stoppable) httpServer)
                        .stop();
            }
        } catch (Exception ex) {
            if (debug) logger.debug("Unable to cleanly shutdown httpServer", ex);
        }

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

    }

    /**
     * Sets up the response queue listener so we can sendText responses
     * to HTTP / WebSocket end points.
     */
    private void startResponseQueueListener() {
        serviceBundle.startReturnHandlerProcessor();
        serviceBundle.startWebResponseReturnHandler(createResponseQueueListener());
    }

    /**
     * Creates the queue listener for method call responses from the client bundle.
     *
     * @return the response queue listener to handle the responses to method calls.
     */
    private ReceiveQueueListener<Response<Object>> createResponseQueueListener() {
        return new ReceiveQueueListener<Response<Object>>() {

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

                if (debug) {
                    logger.debug("createResponseQueueListener() Received a response: " + response);
                }
                handleResponseFromServiceBundle(response, response.request().originatingRequest());
            }

            @Override
            public void limit() {
                httpRequestServerHandler.checkTimeoutsForRequests();
                webSocketHandler.checkResponseBatchSend();
            }

            @Override
            public void empty() {
                httpRequestServerHandler.checkTimeoutsForRequests();
                webSocketHandler.checkResponseBatchSend();
            }

            @Override
            public void idle() {
                httpRequestServerHandler.checkTimeoutsForRequests();
                webSocketHandler.checkResponseBatchSend();
            }
        };
    }

    private void handleResponseFromServiceBundle(final Response<Object> response, final Request<Object> originatingRequest) {

        if (originatingRequest instanceof HttpRequest) {

            if (originatingRequest.isHandled()) {
                return; // the operation timed out
            }
            originatingRequest.handled(); //Let others know that it is handled.

            httpRequestServerHandler.handleResponseFromServiceToHttpResponse(response, (HttpRequest) originatingRequest);
        } else if (originatingRequest instanceof WebSocketMessage) {
            originatingRequest.handled(); //Let others know that it is handled.

            webSocketHandler.handleResponseFromServiceBundleToWebSocketSender(response, (WebSocketMessage) originatingRequest);
        } else {

            throw new IllegalStateException("Unknown response " + response);
        }
    }

    @Override
    public ServiceEndpointServer flush() {
        this.serviceBundle.flush();
        return this;
    }

    @Override
    public ServiceEndpointServer initServices(final Iterable<Object> services) {

        for (Object service : services) {
            if (debug) logger.debug("registering service: " + service.getClass().getName());
            serviceBundle.addService(service);
            httpRequestServerHandler.addRestSupportFor(service.getClass(), serviceBundle.address());
        }
        return this;
    }

    public ServiceEndpointServer addServiceQueue(final String address, final ServiceQueue serviceQueue) {
        serviceBundle().addServiceQueue(address, serviceQueue);
        httpRequestServerHandler.addRestSupportFor(serviceQueue.service().getClass(), serviceBundle().address());
        return this;
    }

    @Override
    public ServiceEndpointServer initServices(Object... services) {
        initServices(Arrays.asList(services));
        return this;
    }

    @Override
    public ServiceEndpointServer addServiceObject(final String address, final Object serviceObject) {
        this.addServiceObjectWithQueueCallBackHandlers(address, serviceObject, (QueueCallBackHandler[]) null);
        return this;
    }

    @Override
    public ServiceEndpointServer addServiceObjectWithQueueCallBackHandlers(final String address, final Object serviceObject,
                                                                           final QueueCallBackHandler... queueCallBackHandlers) {
        if (debug) logger.debug("registering service: " + serviceObject.getClass().getName());
        serviceBundle.addServiceObjectWithQueueCallBackHandlers(address, serviceObject, queueCallBackHandlers);
        httpRequestServerHandler.addRestSupportFor(address, serviceObject.getClass(), serviceBundle.address());
        return this;
    }

    @Override
    public ServiceEndpointServer addService(final Object serviceObject) {
        serviceBundle.addService(serviceObject);
        httpRequestServerHandler.addRestSupportFor(serviceObject.getClass(), serviceBundle.address());
        return this;
    }

    @Override
    public ServiceEndpointServer addServiceWithQueueCallBackHandlers(final Object serviceObject,
                                                                     final QueueCallBackHandler... queueCallBackHandlers) {
        serviceBundle.addServiceWithQueueCallBackHandlers(serviceObject, queueCallBackHandlers);
        httpRequestServerHandler.addRestSupportFor(serviceObject.getClass(), serviceBundle.address());
        return this;
    }

    public ServiceBundle serviceBundle() {
        return this.serviceBundle;
    }

}


package io.advantageous.qbit.queue;

import io.advantageous.qbit.QBit;
import io.advantageous.qbit.concurrent.PeriodicScheduler;
import io.advantageous.qbit.json.JsonMapper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * Wraps a QBit Queue and converts items into JSON and from JSON.
 *
 * @param <T> convert to this type.
 */
public class JsonQueue<T> implements Queue<T> {

    /**
     * Queue we are wrapping with JSON encoding / decoding.
     */
    private final Queue<String> queue;

    /**
     * Decoder.
     */
    private final Function<String, T> fromJsonFunction;

    /**
     * Encoder.
     */
    private final Function<T, String> toJsonFunction;

    /**
     * @param queue            queue
     * @param fromJsonFunction fromJsonFunction function decoder
     * @param toJsonFunction   toJsonFunction function encoder
     */
    public JsonQueue(final Queue<String> queue,
                     final Function<String, T> fromJsonFunction,
                     final Function<T, String> toJsonFunction) {
        this.queue = queue;
        this.fromJsonFunction = fromJsonFunction;
        this.toJsonFunction = toJsonFunction;
    }

    /**
     * Create a simple JsonQueue that encodes one object via JSON.
     *
     * @param classType  classType
     * @param queue      queue
     * @param jsonMapper jsonMapper
     */
    public JsonQueue(final Class<T> classType,
                     final Queue<String> queue,
                     final JsonMapper jsonMapper) {
        this(queue, json -> jsonMapper.fromJson(json, classType), jsonMapper::toJson);
    }

    /**
     * Create a simple JsonQueue that encodes one object via JSON.
     *
     * @param classType classType
     * @param queue     queue
     */
    public JsonQueue(Class<T> classType, Queue<String> queue) {
        this(classType, queue, QBit.factory().createJsonMapper());
    }

    /**
     * @param componentClass component class type
     * @param queue          queue
     * @param jsonMapper     json mapper
     * @param <T>            T
     * @return new JsonQueue that works with lists of componentClass instances
     */
    public static <T> JsonQueue<List<T>> createListQueueWithMapper(final Class<T> componentClass,
                                                                   final Queue<String> queue,
                                                                   final JsonMapper jsonMapper) {
        return new JsonQueue<>(queue,
                json -> jsonMapper.fromJsonArray(json, componentClass),
                jsonMapper::toJson);

    }

    /**
     * @param componentClass component class type
     * @param queue          queue
     * @param <T>            T
     * @return new JsonQueue that works with lists of componentClass instances
     */
    public static <T> JsonQueue<List<T>> createListQueue(final Class<T> componentClass,
                                                         final Queue<String> queue) {

        final JsonMapper jsonMapper = QBit.factory().createJsonMapper();
        return createListQueueWithMapper(componentClass, queue, jsonMapper);

    }

    /**
     * Create a  JsonQueue that can send a Map of values
     *
     * @param mapKeyClass   mapKey class
     * @param valueKeyClass valueKey class
     * @param queue         queue
     * @param jsonMapper    jsonMapper
     * @param <K>           K
     * @param <V>           V
     * @return new JsonQueue that works with maps.
     */
    public static <K, V> JsonQueue<Map<K, V>> createMapQueueWithMapper(
            final Class<K> mapKeyClass,
            final Class<V> valueKeyClass,
            final Queue<String> queue,
            final JsonMapper jsonMapper) {

        return new JsonQueue<>(queue,
                json -> jsonMapper.fromJsonMap(json, mapKeyClass, valueKeyClass),
                jsonMapper::toJson);
    }

    /**
     * Create a  JsonQueue that can send a Map of values
     *
     * @param mapKeyClass   mapKey class
     * @param valueKeyClass valueKey class
     * @param queue         queue
     * @param <K>           K
     * @param <V>           V
     * @return new JsonQueue that works with maps.
     */
    public static <K, V> JsonQueue<Map<K, V>> createMapQueue(final Class<K> mapKeyClass,
                                                             final Class<V> valueKeyClass,
                                                             final Queue<String> queue) {
        final JsonMapper jsonMapper = QBit.factory().createJsonMapper();
        return createMapQueueWithMapper(mapKeyClass, valueKeyClass, queue, jsonMapper);
    }

    /**
     * Create a wrapper ReceiveQueue that does decoding on the fly.
     *
     * @return wrapped ReceiveQueue
     */
    @Override
    public ReceiveQueue<T> receiveQueue() {

        final ReceiveQueue<String> receiveQueue = queue.receiveQueue();

        return new ReceiveQueue<T>() {
            @Override
            public T pollWait() {
                final String item = receiveQueue.pollWait();
                return getParsedItem(item);
            }

            private T getParsedItem(String item) {
                if (item != null) {
                    return fromJsonFunction.apply(item);
                } else {
                    return null;
                }
            }

            @Override
            public T poll() {
                final String item = receiveQueue.pollWait();
                return getParsedItem(item);
            }

            @Override
            public T take() {
                final String item = receiveQueue.take();
                return getParsedItem(item);
            }

            @Override
            public Iterable<T> readBatch(int max) {
                final Iterable<String> iterable = receiveQueue.readBatch(max);
                return getParsedItems(iterable);
            }

            private Iterable<T> getParsedItems(Iterable<String> iterable) {
                int size = 16;
                if (iterable instanceof List) {
                    size = ((List) iterable).size();
                }
                final List<T> items = new ArrayList<>(size);
                for (String item : iterable) {
                    items.add(getParsedItem(item));
                }
                return items;
            }

            @Override
            public Iterable<T> readBatch() {

                final Iterable<String> iterable = receiveQueue.readBatch();
                return getParsedItems(iterable);
            }

            @Override
            public void stop() {
                receiveQueue.stop();
            }
        };
    }

    /**
     * Create a wrapper SendQueue that encoders the objects to JSON
     * before putting them into the queue.
     *
     * @return returns wrapped SendQueue tht does JSON encoding.
     */
    @Override
    public SendQueue<T> sendQueue() {

        final SendQueue<String> sendQueue = queue.sendQueue();

        return createJsonSendQueue(sendQueue);
    }

    /**
     * Helper method to create SendQueue wrappers that do JSON encoding.
     *
     * @param sendQueue sendQueue
     * @return sendQueue decorated with JSON encoding.
     */
    private SendQueue<T> createJsonSendQueue(final SendQueue<String> sendQueue) {

        return new SendQueue<T>() {
            @Override
            public boolean send(T item) {

                sendQueue.send(toJsonFunction.apply(item));
                return false;
            }

            @Override
            public void sendAndFlush(T item) {

                sendQueue.sendAndFlush(toJsonFunction.apply(item));
            }

            @Override
            public void sendMany(T... items) {

                for (T item : items) {
                    sendQueue.send(toJsonFunction.apply(item));
                }
            }

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

                for (T item : items) {
                    sendQueue.send(toJsonFunction.apply(item));
                }
            }

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

                for (T item : items) {
                    sendQueue.send(toJsonFunction.apply(item));
                }
            }

            @Override
            public boolean shouldBatch() {
                return sendQueue.shouldBatch();
            }

            @Override
            public void flushSends() {

                sendQueue.flushSends();
            }

            @Override
            public int size() {
                return sendQueue.size();
            }

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

            @Override
            public void stop() {
                sendQueue.stop();
            }
        };
    }

    /**
     * Create a wrapper SendQueue that encoders the objects to JSON
     * before putting them into the queue.
     *
     * @param interval interval
     * @param timeUnit timeUnit
     * @return returns wrapped SendQueue tht does JSON encoding.
     */
    @Override
    public SendQueue<T> sendQueueWithAutoFlush(int interval, TimeUnit timeUnit) {
        final SendQueue<String> sendQueue = queue.sendQueueWithAutoFlush(interval, timeUnit);
        return createJsonSendQueue(sendQueue);
    }

    /**
     * Create a wrapper SendQueue that encoders the objects to JSON
     * before putting them into the queue.
     *
     * @param periodicScheduler periodicScheduler
     * @param interval          interval
     * @param timeUnit          timeUnit
     * @return sendQueue
     */
    @Override
    public SendQueue<T> sendQueueWithAutoFlush(PeriodicScheduler periodicScheduler, int interval, TimeUnit timeUnit) {
        final SendQueue<String> sendQueue = queue.sendQueueWithAutoFlush(periodicScheduler, interval, timeUnit);
        return createJsonSendQueue(sendQueue);
    }

    /**
     * Start a listener.
     *
     * @param listener listener
     */
    @Override
    public void startListener(final ReceiveQueueListener<T> listener) {
        queue.startListener(item -> listener.receive(fromJsonFunction.apply(item)));
    }

    /**
     * size
     *
     * @return size
     */
    @Override
    public int size() {
        return queue.size();
    }

    /**
     * started
     *
     * @return started
     */
    @Override
    public boolean started() {
        return queue.started();
    }

    /**
     * name of queue
     *
     * @return name
     */
    @Override
    public String name() {
        return queue.name();
    }

    /**
     * Stop the queue.
     */
    @Override
    public void stop() {
        queue.stop();
    }
}


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

package io.advantageous.qbit.service;

import io.advantageous.boon.core.reflection.ClassMeta;
import io.advantageous.boon.core.reflection.MethodAccess;
import io.advantageous.qbit.QBit;
import io.advantageous.qbit.client.BeforeMethodSent;
import io.advantageous.qbit.events.EventManager;
import io.advantageous.qbit.message.MethodCall;
import io.advantageous.qbit.message.Request;
import io.advantageous.qbit.message.Response;
import io.advantageous.qbit.queue.Queue;
import io.advantageous.qbit.queue.QueueBuilder;
import io.advantageous.qbit.queue.QueueCallBackHandler;
import io.advantageous.qbit.queue.ReceiveQueueListener;
import io.advantageous.qbit.queue.impl.QueueCallBackHandlerHub;
import io.advantageous.qbit.service.health.HealthServiceAsync;
import io.advantageous.qbit.service.health.ServiceHealthListener;
import io.advantageous.qbit.service.impl.*;
import io.advantageous.qbit.service.stats.ServiceQueueSizer;
import io.advantageous.qbit.service.stats.ServiceStatsListener;
import io.advantageous.qbit.service.stats.StatsCollector;
import io.advantageous.qbit.system.QBitSystemManager;
import io.advantageous.qbit.transforms.NoOpResponseTransformer;
import io.advantageous.qbit.transforms.Transformer;
import io.advantageous.qbit.util.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Builds a service queue for a service that sits behind a queue.
 * created by rhightower on 1/28/15.
 */
public class ServiceBuilder {

    private final Logger logger = LoggerFactory.getLogger(ServiceBuilder.class);
    private final boolean debug = logger.isDebugEnabled();
    private boolean handleCallbacks;
    private ServiceMethodHandler serviceMethodHandler;
    private BeforeMethodCall beforeMethodCall = ServiceConstants.NO_OP_BEFORE_METHOD_CALL;
    private BeforeMethodCall beforeMethodCallAfterTransform = ServiceConstants.NO_OP_BEFORE_METHOD_CALL;
    private AfterMethodCall afterMethodCall = new NoOpAfterMethodCall();
    private AfterMethodCall afterMethodCallAfterTransform = new NoOpAfterMethodCall();
    private ReceiveQueueListener<MethodCall<Object>> inputQueueListener = new NoOpInputMethodCallQueueListener();
    private Transformer<Request, Object> requestObjectTransformer = ServiceConstants.NO_OP_ARG_TRANSFORM;
    private Transformer<Response<Object>, Response> responseObjectTransformer = new NoOpResponseTransformer();
    private Queue<Response<Object>> responseQueue;
    private QueueBuilder requestQueueBuilder;
    private QueueBuilder responseQueueBuilder;
    private boolean asyncResponse = true;
    private boolean invokeDynamic = true;
    private String rootAddress;
    private String serviceAddress;
    private Object serviceObject;
    private QBitSystemManager qBitSystemManager;
    private List<QueueCallBackHandler> queueCallBackHandlers;
    private Timer timer;
    private StatsConfig statsConfig;

    private CallbackManager callbackManager;
    private CallbackManagerBuilder callbackManagerBuilder;

    private boolean createCallbackHandler = true;
    private EventManager eventManager;
    private BeforeMethodSent beforeMethodSent;
    private boolean joinEventManager = true;

    public static ServiceBuilder serviceBuilder() {
        return new ServiceBuilder();
    }

    public boolean isCreateCallbackHandler() {
        return createCallbackHandler;
    }

    public ServiceBuilder setCreateCallbackHandler(boolean createCallbackHandler) {
        this.createCallbackHandler = createCallbackHandler;
        return this;
    }

    public CallbackManagerBuilder getCallbackManagerBuilder() {
        if (callbackManagerBuilder == null && isCreateCallbackHandler()) {
            callbackManagerBuilder = CallbackManagerBuilder.callbackManagerBuilder();
            if (serviceObject != null) {
                callbackManagerBuilder.setName(serviceObject.getClass().getSimpleName());
            }
        }
        return callbackManagerBuilder;
    }

    public ServiceBuilder setCallbackManagerBuilder(CallbackManagerBuilder callbackManagerBuilder) {
        this.callbackManagerBuilder = callbackManagerBuilder;
        return this;
    }

    public CallbackManager getCallbackManager() {
        if (callbackManager == null && isCreateCallbackHandler()) {
            callbackManager = this.getCallbackManagerBuilder().build();
        }
        return callbackManager;
    }

    public ServiceBuilder setCallbackManager(CallbackManager callbackManager) {
        this.callbackManager = callbackManager;
        return this;
    }

    public List<QueueCallBackHandler> getQueueCallBackHandlers() {
        if (queueCallBackHandlers == null) {
            queueCallBackHandlers = new ArrayList<>();
        }
        return queueCallBackHandlers;
    }

    public ServiceBuilder setQueueCallBackHandlers(List<QueueCallBackHandler> queueCallBackHandlers) {
        this.queueCallBackHandlers = queueCallBackHandlers;
        return this;
    }

    public ServiceBuilder addQueueCallbackHandler(final QueueCallBackHandler queueCallBackHandler) {
        getQueueCallBackHandlers().add(queueCallBackHandler);
        return this;
    }

    public ServiceBuilder addQueueProcessListener(final Runnable runnable) {
        getQueueCallBackHandlers().add(
                new QueueCallBackHandler() {
                    @Override
                    public void queueProcess() {
                        runnable.run();
                    }
                }
        );
        return this;
    }

    private QueueCallBackHandler buildQueueCallBackHandler() {
        if (queueCallBackHandlers == null || queueCallBackHandlers.size() == 0) {
            return new QueueCallBackHandler() {
                @Override
                public void queueLimit() {

                }

                @Override
                public void queueEmpty() {

                }
            };
        } else {
            return new QueueCallBackHandlerHub(queueCallBackHandlers);
        }
    }

    public QBitSystemManager getSystemManager() {
        return qBitSystemManager;
    }

    public ServiceBuilder setSystemManager(QBitSystemManager qBitSystemManager) {
        this.qBitSystemManager = qBitSystemManager;
        return this;
    }

    public QueueBuilder getResponseQueueBuilder() {
        if (responseQueueBuilder == null) {
            this.responseQueueBuilder = QueueBuilder.queueBuilder();
        }
        return responseQueueBuilder;
    }

    public ServiceBuilder setResponseQueueBuilder(QueueBuilder responseQueueBuilder) {
        this.responseQueueBuilder = responseQueueBuilder;
        return this;
    }

    public Queue<Response<Object>> getResponseQueue() {
        return responseQueue;
    }

    public ServiceBuilder setResponseQueue(Queue<Response<Object>> responseQueue) {
        this.responseQueue = responseQueue;
        return this;
    }

    public String getServiceAddress() {
        return serviceAddress;
    }

    public ServiceBuilder setServiceAddress(String serviceAddress) {
        this.serviceAddress = serviceAddress;
        return this;
    }

    public Object getServiceObject() {
        return serviceObject;
    }

    public ServiceBuilder setServiceObject(final Object serviceObject) {

        if (serviceObject == null) {
            throw new IllegalArgumentException("ServiceBuilder setServiceObject:: serviceObject cant be null");
        }

        ClassMeta<?> classMeta = ClassMeta.classMeta(serviceObject.getClass());

        Iterable<MethodAccess> methods = classMeta.methods();

        Set<String> methodNames = new HashSet<>();

        for (MethodAccess methodAccess : methods) {

            if (methodAccess.isPrivate()
                    || methodAccess.method().getDeclaringClass().getName().contains("$$EnhancerByGuice$$")) {
                continue;
            }

            if (methodNames.contains(methodAccess.name())) {
                logger.error("QBit does not support method overloading methods in Service Queues" +
                        " problem name "
                        + methodAccess.name() + " is overloaded " + methodNames + " from class "
                        + classMeta.longName());

            } else {
                methodNames.add(methodAccess.name());
            }

        }

        this.serviceObject = serviceObject;
        return this;
    }

    public String getRootAddress() {
        return rootAddress;
    }

    public ServiceBuilder setRootAddress(String rootAddress) {
        this.rootAddress = rootAddress;
        return this;
    }

    public ServiceMethodHandler getServiceMethodHandler() {

        if (serviceMethodHandler == null) {
            serviceMethodHandler =
                    QBit.factory().createServiceMethodHandler(this.isInvokeDynamic());
        }
        return serviceMethodHandler;
    }

    public ServiceBuilder setServiceMethodHandler(ServiceMethodHandler serviceMethodHandler) {
        this.serviceMethodHandler = serviceMethodHandler;
        return this;

    }

    public BeforeMethodCall getBeforeMethodCall() {

        if (beforeMethodCall == null) {
            beforeMethodCall = new NoOpBeforeMethodCall();
        }
        return beforeMethodCall;
    }

    public ServiceBuilder setBeforeMethodCall(BeforeMethodCall beforeMethodCall) {
        this.beforeMethodCall = beforeMethodCall;
        return this;

    }

    public BeforeMethodCall getBeforeMethodCallAfterTransform() {
        return beforeMethodCallAfterTransform;
    }

    public ServiceBuilder setBeforeMethodCallAfterTransform(BeforeMethodCall beforeMethodCallAfterTransform) {
        this.beforeMethodCallAfterTransform = beforeMethodCallAfterTransform;
        return this;

    }

    public AfterMethodCall getAfterMethodCall() {
        if (afterMethodCall == null) {
            afterMethodCall = new NoOpAfterMethodCall();
        }
        return afterMethodCall;
    }

    public ServiceBuilder setAfterMethodCall(AfterMethodCall afterMethodCall) {
        this.afterMethodCall = afterMethodCall;
        return this;

    }

    public AfterMethodCall getAfterMethodCallAfterTransform() {
        return afterMethodCallAfterTransform;
    }

    public ServiceBuilder setAfterMethodCallAfterTransform(AfterMethodCall afterMethodCallAfterTransform) {
        this.afterMethodCallAfterTransform = afterMethodCallAfterTransform;
        return this;

    }

    public ReceiveQueueListener<MethodCall<Object>> getInputQueueListener() {
        return inputQueueListener;
    }

    public ServiceBuilder setInputQueueListener(ReceiveQueueListener<MethodCall<Object>> inputQueueListener) {
        this.inputQueueListener = inputQueueListener;
        return this;

    }

    public Transformer<Request, Object> getRequestObjectTransformer() {
        return requestObjectTransformer;
    }

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

    }

    public Transformer<Response<Object>, Response> getResponseObjectTransformer() {
        return responseObjectTransformer;
    }

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

    }

    public QueueBuilder getRequestQueueBuilder() {
        if (requestQueueBuilder == null) {
            requestQueueBuilder = QueueBuilder.queueBuilder();
        }
        return requestQueueBuilder;
    }

    public ServiceBuilder setRequestQueueBuilder(QueueBuilder requestQueueBuilder) {
        this.requestQueueBuilder = requestQueueBuilder;
        return this;

    }

    public boolean isAsyncResponse() {
        return asyncResponse;
    }

    public ServiceBuilder setAsyncResponse(boolean asyncResponse) {
        this.asyncResponse = asyncResponse;
        return this;

    }

    public boolean isInvokeDynamic() {
        return invokeDynamic;
    }

    public ServiceBuilder setInvokeDynamic(boolean invokeDynamic) {
        this.invokeDynamic = invokeDynamic;
        return this;
    }

    public boolean isHandleCallbacks() {
        return handleCallbacks;
    }

    public ServiceBuilder setHandleCallbacks(final boolean handleCallbacks) {
        this.handleCallbacks = handleCallbacks;
        return this;
    }

    public Timer getTimer() {
        if (timer == null) {
            timer = Timer.timer();
        }
        return timer;
    }

    public ServiceBuilder setTimer(Timer timer) {
        this.timer = timer;
        return this;
    }

    public ServiceBuilder registerHealthChecks(
            final HealthServiceAsync healthServiceAsync,
            final String serviceName) {
        this.addQueueCallbackHandler(new ServiceHealthListener(serviceName, healthServiceAsync,
                getTimer(), 5, 10, TimeUnit.SECONDS, new ServiceHealthManagerDefault(null, null)));
        return this;
    }

    public ServiceBuilder registerStatsCollections(
            final String serviceName,
            final StatsCollector statsCollector,
            final int flushTimeSeconds,
            final int sampleEvery) {

        statsConfig = new StatsConfig(serviceName, statsCollector, flushTimeSeconds, sampleEvery);
        return this;
    }

    public ServiceBuilder registerHealthChecksWithTTLInSeconds(
            final HealthServiceAsync healthServiceAsync,
            final String serviceName, final int seconds) {

        int ttl = seconds > 2 ? seconds : 10;

        int checkInterval = (ttl / 2 == 0) ? 1 : ttl / 2;

        this.addQueueCallbackHandler(new ServiceHealthListener(serviceName, healthServiceAsync,
                getTimer(), checkInterval, ttl, TimeUnit.SECONDS, new ServiceHealthManagerDefault(null, null)));
        return this;
    }

    public ServiceQueue build(final Object serviceObject) {
        this.serviceObject = serviceObject;
        return build();
    }

    /**
     * Builds a service.
     *
     * @return new service queue
     */
    public ServiceQueue build() {

        if (debug) logger.debug("Building a service");

        ServiceQueueSizer serviceQueueSizer = null;

        if (statsConfig != null) {

            serviceQueueSizer = new ServiceQueueSizer();
            this.addQueueCallbackHandler(new ServiceStatsListener(statsConfig.serviceName,
                    statsConfig.statsCollector,
                    getTimer(), statsConfig.flushTimeSeconds, TimeUnit.SECONDS,
                    statsConfig.sampleEvery, serviceQueueSizer));
        }

        ServiceQueue serviceQueue = new ServiceQueueImpl(this.getRootAddress(),
                this.getServiceAddress(),
                this.getServiceObject(),
                this.getRequestQueueBuilder(),
                this.getResponseQueueBuilder(),
                this.getServiceMethodHandler(),
                this.getResponseQueue(),
                this.isAsyncResponse(),
                this.isHandleCallbacks(),
                this.getSystemManager(),
                this.getBeforeMethodCall(),
                this.getBeforeMethodCallAfterTransform(),
                this.getAfterMethodCall(),
                this.getAfterMethodCallAfterTransform(),
                buildQueueCallBackHandler(),
                getCallbackManager(),
                getBeforeMethodSent(),
                getEventManager(),
                isJoinEventManager()
        );

        if (serviceQueueSizer != null) {
            serviceQueueSizer.setServiceQueue(serviceQueue);
        }

        if (qBitSystemManager != null) {
            qBitSystemManager.registerService(serviceQueue);
        }

        return serviceQueue;
    }

    /**
     * Builds and starts the service queue.
     *
     * @return the service queue
     */
    public ServiceQueue buildAndStart() {

        return build().startServiceQueue();
    }

    public ServiceQueue buildAndStartAll() {

        this.setCreateCallbackHandler(true);
        return build().startAll();
    }

    public EventManager getEventManager() {
        return eventManager;
    }

    public ServiceBuilder setEventManager(EventManager eventManager) {
        this.eventManager = eventManager;
        return this;
    }

    public BeforeMethodSent getBeforeMethodSent() {
        return beforeMethodSent;
    }

    public ServiceBuilder setBeforeMethodSent(final BeforeMethodSent beforeMethodSent) {
        this.beforeMethodSent = beforeMethodSent;
        return this;
    }

    public ServiceBuilder copy() {
        ServiceBuilder serviceBuilder = new ServiceBuilder();
        serviceBuilder.setAfterMethodCall(this.getAfterMethodCall());
        serviceBuilder.setBeforeMethodCall(this.getBeforeMethodCall());
        serviceBuilder.setAsyncResponse(this.isAsyncResponse());
        serviceBuilder.setEventManager(this.getEventManager());
        serviceBuilder.setHandleCallbacks(this.handleCallbacks);
        serviceBuilder.setSystemManager(this.getSystemManager());
        serviceBuilder.setBeforeMethodCallAfterTransform(this.getBeforeMethodCallAfterTransform());
        serviceBuilder.setResponseQueueBuilder(this.getResponseQueueBuilder());
        serviceBuilder.setRequestQueueBuilder(this.getRequestQueueBuilder());
        return serviceBuilder;
    }

    public boolean isJoinEventManager() {
        return joinEventManager;
    }

    public ServiceBuilder setJoinEventManager(boolean joinEventManager) {
        this.joinEventManager = joinEventManager;
        return this;
    }

    private static class StatsConfig {
        final String serviceName;
        final StatsCollector statsCollector;
        final int flushTimeSeconds;
        final int sampleEvery;

        private StatsConfig(String serviceName, StatsCollector statsCollector, int flushTimeSeconds, int sampleEvery) {
            this.serviceName = serviceName;
            this.statsCollector = statsCollector;
            this.flushTimeSeconds = flushTimeSeconds;
            this.sampleEvery = sampleEvery;
        }
    }
}


package io.advantageous.qbit.service.health;

import io.advantageous.boon.core.Lists;
import io.advantageous.boon.core.reflection.BeanUtils;
import io.advantageous.qbit.reactive.Reactor;
import io.advantageous.qbit.service.BaseService;
import io.advantageous.qbit.service.Stoppable;
import io.advantageous.qbit.service.stats.StatsCollector;
import io.advantageous.qbit.util.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * Manages health status of internal nodes/services.
 */
public class HealthServiceImpl extends BaseService implements HealthService, Stoppable {

    private static int healthServiceCount;
    /**
     * Internal map to check health.
     */
    private final Map<String, NodeHealthStat> serviceHealthStatMap
            = new ConcurrentHashMap<>();
    /**
     * logger.
     */
    private final Logger logger = LoggerFactory.getLogger(HealthServiceImpl.class);
    private final boolean debug = logger.isDebugEnabled();
    private final Optional<Consumer<NodeHealthStat>> onFail;
    private final Optional<Consumer<NodeHealthStat>> onWarn;
    private final Optional<Consumer<NodeHealthStat>> onCheckIn;

    private final List<HealthCheckJob> healthCheckJobs;

    /**
     * Constructor.
     *
     * @param healthPrefix    health prefix
     * @param reactor         reactor
     * @param timer           timer
     * @param statsCollector  stats collector.
     * @param recheckInterval recheck interval
     * @param timeUnit        time unit for interval
     * @param healthCheckJobs jobs that perform health checks
     * @param onFail          onFail
     * @param onWarn          onWarn
     * @param onCheckIn       onCheckIn
     */
    public HealthServiceImpl(final String healthPrefix,
                             final Reactor reactor,
                             final Timer timer,
                             final StatsCollector statsCollector,
                             final long recheckInterval,
                             final TimeUnit timeUnit,
                             final List<HealthCheckJob> healthCheckJobs,
                             final Optional<Consumer<NodeHealthStat>> onFail,
                             final Optional<Consumer<NodeHealthStat>> onWarn,
                             final Optional<Consumer<NodeHealthStat>> onCheckIn) {

        super(healthPrefix, reactor, timer, statsCollector);
        this.onFail = onFail;
        this.onWarn = onWarn;
        this.onCheckIn = onCheckIn;

        reactor.addRepeatingTask(recheckInterval, timeUnit, () -> checkTTLs());

        this.healthCheckJobs = Collections.unmodifiableList(healthCheckJobs);

        healthCheckJobs.forEach(healthCheckJob -> reactor.addRepeatingTask(healthCheckJob.getDuration(),
                () -> runHealthCheck(healthCheckJob)));

        healthServiceCount++;

        if (logger.isDebugEnabled()) {
            Exception ex = new Exception("Health Service init called");
            ex.fillInStackTrace();
            logger.debug("Health Service CREATED", ex);
        }

        if (healthServiceCount > 1) {
            logger.info("More than ONE Health Service created {}, if that is not intended turn on debugging", healthServiceCount);
        }

        logger.info("Health Service CREATED {}", this.hashCode());
    }

    private void runHealthCheck(final HealthCheckJob healthCheckJob) {
        final NodeHealthStat currentHealth = healthCheckJob.getHealthCheck().check();

        reportStatus(healthCheckJob.getName(), currentHealth);
        serviceHealthStatMap.put(healthCheckJob.getName(), currentHealth);
    }

    private void reportStatus(String name, NodeHealthStat currentHealth) {
        switch (currentHealth.getStatus()) {
            case PASS:
                if (debug) logger.debug("HEALTH PASS :: {} status check and got status {} ", name, currentHealth);
                onCheckIn.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(currentHealth)));
                super.incrementCount("pass");
                break;

            case FAIL:
                logger.error("HEALTH FAIL :: {} status check and got status {} ", name, currentHealth);
                onFail.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(currentHealth)));
                super.incrementCount("fail");
                break;

            case WARN:
                logger.warn("HEALTH WARNING :: {} status check and got status {} ", name, currentHealth);
                onWarn.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(currentHealth)));
                super.incrementCount("warn");
                break;
            default:
                onCheckIn.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(currentHealth)));
        }
    }

    /**
     * Register method to register services / internal nodes.
     *
     * @param name     name
     * @param ttl      ttl
     * @param timeUnit timeUnit
     */
    @Override
    public void register(final String name, final long ttl, final TimeUnit timeUnit) {

        logger.info("HealthService::register() {} {} {}", name, ttl, timeUnit);
        final NodeHealthStat nodeHealthStat = new NodeHealthStat(name, timeUnit.toMillis(ttl));
        serviceHealthStatMap.put(name, nodeHealthStat);
        super.incrementCount("nodes");
        super.recordLevel("nodes", serviceHealthStatMap.size());

    }

    /**
     * Register method to register services / internal nodes.
     *
     * @param name name
     */
    @Override
    public void registerNoTtl(final String name) {

        logger.info("HealthService::register() {} ", name);
        final NodeHealthStat nodeHealthStat = new NodeHealthStat(name);
        nodeHealthStat.setStatus(HealthStatus.PASS);
        serviceHealthStatMap.put(name, nodeHealthStat);
        super.incrementCount("nodes");
        super.recordLevel("nodes", serviceHealthStatMap.size());

    }

    /**
     * Check in the service.
     *
     * @param name name
     */
    @Override
    public void checkInOk(final String name) {

        if (debug) logger.debug("HealthService::checkInOk() {} ", name);
        final NodeHealthStat nodeHealthStat = getServiceHealthStat(name);

        nodeHealthStat.setLastCheckIn(super.time);
        nodeHealthStat.setReason(null);
        nodeHealthStat.setStatus(HealthStatus.PASS);

        super.incrementCount("checkin");

        onCheckIn.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(nodeHealthStat)));

    }

    /**
     * Check in the service with a specific status.
     *
     * @param name   name
     * @param status status
     */
    @Override
    public void checkIn(final String name, final HealthStatus status) {

        super.incrementCount("checkin");

        final NodeHealthStat nodeHealthStat = getServiceHealthStat(name);

        nodeHealthStat.setStatus(status);
        nodeHealthStat.setReason(null);
        nodeHealthStat.setLastCheckIn(super.time);

        reportStatus(name, nodeHealthStat);
    }

    /**
     * Check in the service with a specific status.
     *
     * @param name   name
     * @param reason reason
     */
    @Override
    public void failWithReason(final String name, final HealthFailReason reason) {
        logger.error("HealthService::fail() {}", name);

        super.incrementCount("fail");

        final NodeHealthStat nodeHealthStat = getServiceHealthStat(name);

        nodeHealthStat.setStatus(HealthStatus.FAIL);
        nodeHealthStat.setReason(reason);
        nodeHealthStat.setLastCheckIn(super.time);

        onFail.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(nodeHealthStat)));
    }

    /**
     * Fail the node for the service with a specific status.
     *
     * @param name  name
     * @param error error
     */
    @Override
    public void failWithError(final String name, final Throwable error) {
        logger.error("HealthService::fail() {}", name);

        super.incrementCount("fail");

        final NodeHealthStat nodeHealthStat = getServiceHealthStat(name);

        nodeHealthStat.setStatus(HealthStatus.FAIL);
        nodeHealthStat.setReason(HealthFailReason.ERROR);
        nodeHealthStat.setLastCheckIn(super.time);
        nodeHealthStat.setError(error);

        onFail.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(nodeHealthStat)));

    }

    @Override
    public void warnWithReason(final String name, final HealthFailReason reason) {
        logger.warn("HealthService::warn() {}", name);

        super.incrementCount("warn");

        final NodeHealthStat nodeHealthStat = getServiceHealthStat(name);

        nodeHealthStat.setStatus(HealthStatus.WARN);
        nodeHealthStat.setReason(reason);
        nodeHealthStat.setLastCheckIn(super.time);

        onWarn.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(nodeHealthStat)));

    }

    @Override
    public void warnWithError(final String name, final Throwable error) {
        logger.warn("HealthService::warn() {}", name);

        super.incrementCount("warn");

        final NodeHealthStat nodeHealthStat = getServiceHealthStat(name);

        nodeHealthStat.setStatus(HealthStatus.WARN);
        nodeHealthStat.setReason(HealthFailReason.ERROR);
        nodeHealthStat.setLastCheckIn(super.time);
        nodeHealthStat.setError(error);

        onWarn.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(nodeHealthStat)));

    }

    @Override
    public boolean ok() {
        if (debug) logger.debug("HealthService::ok()");

        boolean ok = serviceHealthStatMap.values()
                .stream()
                .allMatch(serviceHealthStat -> serviceHealthStat.getStatus() == HealthStatus.PASS);

        if (debug) logger.debug("HealthService::ok() was ok? {}", ok);
        return ok;
    }

    @Override
    public List<String> findHealthyNodes() {

        logger.info("HealthService::findHealthyNodes() called");
        final List<String> names = new ArrayList<>();

        serviceHealthStatMap.values()
                .stream()
                .filter(serviceHealthStat -> serviceHealthStat.getStatus() == HealthStatus.PASS)
                .forEach(serviceHealthStat -> names.add(serviceHealthStat.getName()));

        logger.info("HealthService::findHealthyNodes() called returns {}", names);
        return names;
    }

    @Override
    public List<String> findAllNodes() {

        logger.info("HealthService::findAllNodes() called");
        final List<String> names = new ArrayList<>();

        serviceHealthStatMap.values()
                .stream()
                .forEach(serviceHealthStat -> names.add(serviceHealthStat.getName()));

        logger.info("HealthService::findAllNodes() called returns {}", names);
        return names;
    }

    @Override
    public List<String> findAllNodesWithStatus(final HealthStatus queryStatus) {

        final List<String> names = new ArrayList<>();

        serviceHealthStatMap.values()
                .stream()
                .filter(serviceHealthStat -> serviceHealthStat.getStatus() == queryStatus)
                .forEach(serviceHealthStat -> names.add(serviceHealthStat.getName()));

        return names;
    }

    @Override
    public List<String> findNotHealthyNodes() {

        final List<String> names = new ArrayList<>();

        serviceHealthStatMap.values()
                .stream()
                .filter(serviceHealthStat -> serviceHealthStat.getStatus() != HealthStatus.PASS)
                .forEach(serviceHealthStat -> names.add(serviceHealthStat.getName()));

        return names;
    }

    @Override
    public List<NodeHealthStat> loadNodes() {

        logger.info("HealthService::loadNodes() called");
        return Lists.deepCopy(this.serviceHealthStatMap.values());
    }

    @Override
    public void unregister(String nodeName) {

        super.incrementCount("unregister");
        serviceHealthStatMap.remove(nodeName);
    }

    private void checkTTLs() {
        super.incrementCount("ttlcheck");

        final Collection<NodeHealthStat> services = serviceHealthStatMap.values();

        //noinspection Convert2MethodRef
        services.forEach(serviceHealthStat -> checkTTL(serviceHealthStat));
    }

    private void checkTTL(final NodeHealthStat nodeHealthStat) {

        if (debug) logger.debug("HealthService::checkTTL() {}", nodeHealthStat.getName());
        if (!nodeHealthStat.isOk()) {
            return;
        }

        if (nodeHealthStat.isForever()) {
            return;
        }

        final long duration = super.time - nodeHealthStat.getLastCheckIn();

        /* If the duration is greater than the ttl interval, then mark it as failed. */
        if (duration > nodeHealthStat.getTtlInMS()) {

            logger.error("HealthService::checkTTL() {} FAILED TTL check, duration {}",
                    nodeHealthStat.getName(), duration);

            nodeHealthStat.setReason(HealthFailReason.FAILED_TTL);
            nodeHealthStat.setStatus(HealthStatus.FAIL);

            onFail.ifPresent(checkIn -> checkIn.accept(BeanUtils.copy(nodeHealthStat)));

        }
    }

    private NodeHealthStat getServiceHealthStat(final String name) {
        NodeHealthStat nodeHealthStat = serviceHealthStatMap.get(name);

        if (nodeHealthStat == null) {

            logger.warn("Node {} not registered but you are checking in, we will register with NO TTL");
            this.registerNoTtl(name);
            nodeHealthStat = serviceHealthStatMap.get(name);
        }

        return nodeHealthStat;

    }

    void callProcess() {
        super.doProcess();
    }

    @Override
    public void stop() {
        logger.info("Health Service stopped");
    }
}