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

import io.advantageous.qbit.events.EventListener;
import io.advantageous.qbit.message.Event;
import io.advantageous.qbit.service.stats.StatsCollector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * Manages a channel. A channel is like a communication pipe
 *
 * @param <T> type of channel
 * @author rick hightower
 */
public class ChannelManager<T> {

    private final Logger logger = LoggerFactory.getLogger(ChannelManager.class);
    private final boolean debug = logger.isDebugEnabled();
    private final String name;
    private final List<EventListener<T>> listeners;
    private final StatsCollector stats;
    private final String eventBusName;
    private EventListener<T> consumer;
    private int sentCount;
    private String sentCountStatKey;

    public ChannelManager(final String eventBusName, final String name, StatsCollector statsCollector) {
        logger.info("Channel {} was created", name);
        this.name = name;
        consumer = new NoOpEventConsumer<>();
        this.stats = statsCollector;
        this.eventBusName = eventBusName;

        listeners = new ArrayList<>();

        sentCountStatKey = eventBusName + "." + name;

    }

    /**
     * Add an event listener to the channel
     * There can only be one consumer and many listeners.
     *
     * @param eventListener eventListener
     */
    public void add(final EventListener<T> eventListener) {
        if (eventListener.subscriber()) {
            if (debug) logger.debug("subscription to channel <> from <> ", name, eventListener);
            listeners.add(eventListener);
            stats.recordLevel(eventBusName + "::" + name, listeners.size());
        } else {
            if (debug) logger.debug("consumer to channel <> from <> ", name, eventListener);
            consumer = eventListener;
        }
    }

    /**
     * Remove an event listener from the channel.
     * <p>
     * There can only be one consumer and many listeners.
     *
     * @param eventListener eventListener
     */
    public void remove(EventListener<T> eventListener) {

        if (eventListener.subscriber()) {
            logger.info("remove subscription to channel {} from {} ", name, eventListener);
            listeners.remove(eventListener);
            stats.recordLevel(eventBusName + "::" + name, listeners.size());
        } else {

            if (consumer == eventListener) {
                logger.info("remove consumer to channel {} from {} ", name, eventListener);
                consumer = new NoOpEventConsumer<>();
            }
        }
    }

    public void send(final Event<T> event) {

        if (debug) logger.debug("ChannelManager::send() {}", event);

        sentCount++;
        if (sentCount > 20) {
            stats.recordCount(sentCountStatKey, sentCount);
            sentCount = 0;
        }

        for (EventListener<T> listener : listeners) {
            try {
                listener.listen(event);
            } catch (Exception ex) {
                logger.error("Unable to send event for Channel" + name, ex);
            }
        }

        consumer.listen(event);
    }

}


package io.advantageous.qbit.metrics;

import io.advantageous.boon.core.Pair;
import io.advantageous.qbit.GlobalConstants;
import io.advantageous.qbit.annotation.QueueCallback;
import io.advantageous.qbit.annotation.QueueCallbackType;
import io.advantageous.qbit.service.ServiceProxyUtils;
import io.advantageous.qbit.service.discovery.*;
import io.advantageous.qbit.util.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import static io.advantageous.boon.core.Str.sputs;

/**
 * Clustered Stat Replicator
 * created by rhightower on 3/24/15.
 */
public class ClusteredStatReplicator implements StatReplicator, ServiceChangedEventChannel {

    private final ServiceDiscovery serviceDiscovery;
    private final StatReplicatorProvider statReplicatorProvider;
    private final ConcurrentHashMap<String, Pair<EndpointDefinition, StatReplicator>>
            replicatorsMap = new ConcurrentHashMap<>();
    private final Logger logger = LoggerFactory.getLogger(ClusteredStatReplicator.class);
    private final boolean debug = GlobalConstants.DEBUG || logger.isDebugEnabled();
    private final boolean trace = logger.isTraceEnabled();
    private final String serviceName;
    private final ServicePool servicePool;
    private final String localServiceId;
    private final Timer timer;
    private final int tallyInterval;
    private final int flushInterval;
    private final ConcurrentHashMap<String, LocalCount> countMap = new ConcurrentHashMap<>();
    private long currentTime;
    private long lastReconnectTime;
    private long lastSendTime;
    private long lastReplicatorFlush = 0;
    private List<Pair<EndpointDefinition, StatReplicator>> statReplicators = new ArrayList<>();

    public ClusteredStatReplicator(final String serviceName,
                                   final ServiceDiscovery serviceDiscovery,
                                   final StatReplicatorProvider statReplicatorProvider,
                                   final String localServiceId,
                                   final Timer timer,
                                   final int tallyInterval,
                                   final int flushInterval) {
        this.serviceDiscovery = serviceDiscovery;
        this.statReplicatorProvider = statReplicatorProvider;
        this.serviceName = serviceName;
        this.localServiceId = localServiceId;
        this.servicePool = new ServicePool(serviceName, null);
        this.timer = timer;
        this.tallyInterval = tallyInterval;
        this.flushInterval = flushInterval;

    }

    @Override
    public void replicateCount(final String name, final long count, final long now) {

        if (trace) logger.trace(sputs("ClusteredStatReplicator::replicateCount()",
                serviceName, name, count, now));

        if (debug) {
            if (statReplicators.size() == 0) {
                logger.debug(sputs("ClusteredStatReplicator::replicateCount", name, count, now));
            }
        }

        LocalCount localCount = countMap.get(name);

        if (localCount == null) {
            localCount = new LocalCount();
            localCount.name = name;
            countMap.put(name, localCount);
        }
        localCount.count += count;

    }

    @Override
    public void replicateLevel(final String name, long level, long time) {

        LocalCount localCount = countMap.get(name);

        if (localCount == null) {
            localCount = new LocalCount();
            localCount.name = name;
            countMap.put(name, localCount);
        }
        localCount.count = level;
    }

    @Override
    public void replicateTiming(String name, long level, long time) {

        LocalCount localCount = countMap.get(name);

        if (localCount == null) {
            localCount = new LocalCount();
            localCount.name = name;
            countMap.put(name, localCount);
        }
        localCount.count = level;
    }

    private void doRecordCount(Pair<EndpointDefinition, StatReplicator> statReplicator,
                               final String name, final long count, final long now) {

        try {
            statReplicator.getSecond().replicateCount(name, count, now);
        } catch (Exception ex) {
            logger.error(sputs("ClusteredStatReplicator::Replicator failed", statReplicator), ex);
        }
    }

    @QueueCallback({QueueCallbackType.IDLE,
            QueueCallbackType.EMPTY,
            QueueCallbackType.LIMIT})
    void process() {

        currentTime = timer.now();

        sendIfNeeded();
        checkForReconnect();
    }

    private void sendIfNeeded() {

        long duration = currentTime - lastSendTime;

        if (duration > tallyInterval) {
            this.lastSendTime = currentTime;

            final Collection<LocalCount> countCollection = this.countMap.values();

            for (LocalCount localCount : countCollection) {

                if (localCount.count > 0) {
                    statReplicators.forEach(
                            statReplicator -> doRecordCount(statReplicator, localCount.name, localCount.count, currentTime)
                    );
                }
                localCount.count = 0;
            }
            if (countMap.size() > 10_000_000) {
                countMap.clear();
            }
            flushReplicatorsAll();
        }

    }

    private void flushReplicatorsAll() {

        if (currentTime - lastReplicatorFlush > flushInterval) {
            lastReplicatorFlush = currentTime;

            final List<Pair<EndpointDefinition, StatReplicator>> badReplicators = new ArrayList<>();
            statReplicators.forEach(
                    statReplicator -> flushReplicator(statReplicator, badReplicators)
            );
            badReplicators.forEach(statReplicator -> {

                        try {
                            statReplicator.getSecond().stop();
                        } catch (Exception ex) {
                            logger.info("Failed to stop failed node", ex);
                        }
                        statReplicators.remove(statReplicator);
                        replicatorsMap.remove(statReplicator.getFirst().getId());
                    }
            );

            if (trace) {

                logger.trace(sputs("ClusteredStatReplicator::flushReplicatorsAll()",
                        badReplicators.size()));
                badReplicators.forEach(statReplicator -> logger.debug(sputs(statReplicator)));
            }
        }
    }

    private void checkForReconnect() {
        long duration = currentTime - lastReconnectTime;
        if (duration > 60_000) {
            doCheckReconnect();
        }

    }

    public void doCheckReconnect() {

        lastReconnectTime = currentTime;
        final List<EndpointDefinition> services = servicePool.services();

        if ((services.size() - 1) != this.statReplicators.size()) {
            logger.info(sputs("DOING RECONNECT", services.size() - 1,
                    this.statReplicators.size()));

            shutDownReplicators();
            services.forEach(this::addService);
        }
    }

    private void shutDownReplicators() {
        if (debug) logger.debug("Shutting down replicators");
        for (Pair<EndpointDefinition, StatReplicator> statReplicator : statReplicators) {
            try {
                statReplicator.getSecond().stop();

            } catch (Exception ex) {
                logger.debug("Shutdown replicator failed", ex);
            }

            if (debug) logger.debug("Shutting down replicator");
        }
        statReplicators.clear();
        replicatorsMap.clear();
    }

    private void flushReplicator(final Pair<EndpointDefinition, StatReplicator> statReplicator,
                                 final List<Pair<EndpointDefinition, StatReplicator>> badReplicators) {

        try {
            ServiceProxyUtils.flushServiceProxy(statReplicator.getSecond());
        } catch (Exception exception) {
            badReplicators.add(statReplicator);
            logger.info("Replicator failed " + statReplicator, exception);
        }
    }

    /**
     * Event handler
     *
     * @param serviceName service name
     */
    @Override
    public void servicePoolChanged(final String serviceName) {
        if (this.serviceName.equals(serviceName)) {
            logger.info("ClusteredStatReplicator::servicePoolChanged({})", serviceName);
            updateServicePool(serviceName);
        } else if (debug) {
            logger.debug("ClusteredStatReplicator::servicePoolChanged({})",
                    "got event for another service", serviceName);
        }
    }

    private void updateServicePool(final String serviceName) {

        try {
            final List<EndpointDefinition> nodes = serviceDiscovery.loadServices(serviceName);
            servicePool.setHealthyNodes(nodes, new ServicePoolListener() {
                @Override
                public void servicePoolChanged(String serviceName) {
                }

                @Override
                public void serviceAdded(String serviceName, EndpointDefinition endpointDefinition) {
                    addService(endpointDefinition);
                }

                @Override
                public void serviceRemoved(String serviceName, EndpointDefinition endpointDefinition) {
                    removeService(endpointDefinition);
                }

            });
        } catch (Exception ex) {
            logger.error("Error updating service pool");
        }
    }

    private void removeService(final EndpointDefinition endpointDefinition) {

        logger.info(sputs("ClusteredStatReplicator::removeService()",
                serviceName, endpointDefinition, " replicator count ", replicatorsMap.size()));

        final Pair<EndpointDefinition, StatReplicator> statReplicatorPair = this.replicatorsMap.get(endpointDefinition.getId());

        if (statReplicatorPair == null) {

            logger.error(sputs("ClusteredStatReplicator::removeService() Trying to remove a service that we are not managing",
                    serviceName, "END POINT ID", endpointDefinition.getId(), " replicator count ", replicatorsMap.size()));

            return;

        }

        if (statReplicatorPair.getSecond() == null) {

            logger.error(sputs("ClusteredStatReplicator::removeService() Trying to remove a service that we are nto managing" +
                            "and the getSecond() is null",
                    serviceName, "END POINT ID", endpointDefinition.getId(), " replicator count ", replicatorsMap.size()));

            return;

        }

        try {
            statReplicatorPair.getSecond().stop();
        } catch (Exception ex) {
            logger.error("Unable to stop service endpoint that was removed " + endpointDefinition, ex);
        }
        this.replicatorsMap.remove(endpointDefinition.getId());
        this.statReplicators = new ArrayList<>(replicatorsMap.values());

        logger.info(sputs("ClusteredStatReplicator::removeService() removed",
                serviceName, endpointDefinition, " replicator count ", replicatorsMap.size()));

    }

    private void addService(final EndpointDefinition endpointDefinition) {

        if (endpointDefinition.getId().equals(localServiceId)) {
            return;
        }
        logger.info(sputs("ClusteredStatReplicator::addService()", endpointDefinition,
                " replicator count ", replicatorsMap.size()));

        final StatReplicator statReplicator = statReplicatorProvider.provide(endpointDefinition);
        this.replicatorsMap.put(endpointDefinition.getId(), Pair.pair(endpointDefinition, statReplicator));
        this.statReplicators = new ArrayList<>(replicatorsMap.values());

        logger.info(sputs("ClusteredStatReplicator::addService() added",
                serviceName, endpointDefinition, " replicator count ", replicatorsMap.size()));

    }

    @Override
    public void flush() {
        process();
    }

    final static class LocalCount {

        long count;
        String name;

    }
}


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

import io.advantageous.boon.core.IO;
import io.advantageous.boon.core.Str;
import io.advantageous.boon.core.Sys;
import io.advantageous.qbit.service.discovery.ServiceDiscovery;
import io.advantageous.qbit.service.discovery.ServiceDiscoveryBuilder;
import io.vertx.core.Vertx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.stream.Collectors;

/**
 * This is a utility class that reads DNS hosts from /etc/resolv.conf.
 */
public class DnsUtil {

    public static final String QBIT_DNS_RESOLV_CONF = "QBIT_DNS_RESOLV_CONF";

    public static List<URI> readDnsConf() {

        final Logger logger = LoggerFactory.getLogger(DnsUtil.class);

        final boolean debug = logger.isDebugEnabled();

        final File file = new File(Sys.sysProp(QBIT_DNS_RESOLV_CONF, "/etc/resolv.conf"));

        if (file.exists()) {
            final List<String> lines = IO.readLines(file);

            if (debug) logger.debug("file contents {}", lines);

            return lines.stream().filter(line -> line.startsWith("nameserver"))
                    .map(line ->
                    {

                        if (debug) logger.debug("file content line = {}", line);
                        final String uriToParse = line.replace("nameserver ", "").trim();
                        final String[] split = Str.split(uriToParse, ':');
                        try {

                            if (split.length == 1) {
                                return new URI("dns", "", split[0], 53, "", "", "");
                            } else if (split.length >= 2) {
                                return new URI("dns", "", split[0], Integer.parseInt(split[1]), "", "", "");
                            } else {
                                throw new IllegalStateException("Unable to parse URI from /etc/resolv.conf");
                            }
                        } catch (URISyntaxException e) {
                            throw new IllegalStateException("failed to convert to URI");
                        }

                    })
                    .collect(Collectors.toList());
        } else {
            throw new IllegalStateException("" + file + " not found");
        }

    }

    /**
     * Create service discovery that can talk DNS.
     * @return ServiceDiscovery
     */
    public static ServiceDiscovery createDnsServiceDiscovery() {

        final ServiceDiscoveryBuilder serviceDiscoveryBuilder = ServiceDiscoveryBuilder.serviceDiscoveryBuilder();

        final Vertx vertx = Vertx.vertx();
        final DnsSupportBuilder dnsSupportBuilder = DnsSupportBuilder.dnsSupportBuilder(vertx)
                .setDnsClientSupplier(new DnsClientFromResolveConfSupplier(vertx));

        final DnsServiceDiscoveryProviderBuilder dnsServiceDiscoveryProviderBuilder =
                DnsServiceDiscoveryProviderBuilder.dnsServiceDiscoveryProviderBuilder()
                        .setDnsSupport(dnsSupportBuilder.build());

        return serviceDiscoveryBuilder
                .setServiceDiscoveryProvider(dnsServiceDiscoveryProviderBuilder.build()).build();
    }

}


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

import io.advantageous.boon.core.Conversions;
import io.advantageous.boon.core.Sets;
import io.advantageous.boon.core.Value;
import io.advantageous.boon.core.reflection.Mapper;
import io.advantageous.boon.core.reflection.MapperComplex;
import io.advantageous.boon.core.reflection.fields.FieldAccessMode;
import io.advantageous.boon.core.value.ValueContainer;
import io.advantageous.boon.core.value.ValueMap;
import io.advantageous.boon.json.JsonParserAndMapper;
import io.advantageous.boon.json.JsonParserFactory;
import io.advantageous.boon.json.JsonSerializer;
import io.advantageous.boon.json.JsonSerializerFactory;
import io.advantageous.qbit.json.JsonMapper;

import java.util.*;
import java.util.function.Consumer;

/**
 * JsonMapper is the primary interface to provide JSON serialization and deserialization for QBit.
 * The default implementation of the JsonMapper is BoonJsonMapper.
 * <p>
 * BoonJsonMapper is thread safe.
 * created by gcc on 10/15/14.
 *
 * @author Rick Hightower
 */
public class BoonJsonMapper implements JsonMapper {

    /**
     * Holds the JsonParserAndMapper parser to parse JSON.
     */
    private final ThreadLocal<JsonParserAndMapper> parser = new ThreadLocal<JsonParserAndMapper>() {
        @Override
        protected JsonParserAndMapper initialValue() {
            return new JsonParserFactory().setIgnoreSet(Sets.set("metaClass")).createFastObjectMapperParser();
        }
    };

    /**
     * Holds the JsonSerializer to deserialize JSON into Java objects.
     */
    private final ThreadLocal<JsonSerializer> serializer = new ThreadLocal<JsonSerializer>() {
        @Override
        protected JsonSerializer initialValue() {
            return new JsonSerializerFactory().setUseAnnotations(true)
                    .addFilter((parent, fieldAccess) -> !fieldAccess.name().equals("metaClass")).create();
        }
    };

    /**
     * Holds the Mapper to convert Maps into Java objects.
     */
    private final ThreadLocal<Mapper> mapper = new ThreadLocal<Mapper>() {
        @Override
        protected Mapper initialValue() {

            /**
             * MapperComplex(FieldAccessMode fieldAccessType, boolean useAnnotations,
             boolean caseInsensitiveFields, Set<String> ignoreSet,
             String view, boolean respectIgnore, boolean acceptSingleValueAsArray) {
             fieldsAccessor = FieldAccessMode.create( fieldAccessType, useAnnotations, caseInsensitiveFields );
             this.ignoreSet = ignoreSet;
             this.view = view;
             this.respectIgnore = respectIgnore;
             this.acceptSingleValueAsArray = acceptSingleValueAsArray;
             this.outputType = true;
             }
             */
            return new MapperComplex(false, FieldAccessMode.PROPERTY_THEN_FIELD, true, false, Collections.emptySet(), null, true, true);
        }
    };

    /**
     * Convert a JSON string into one Java Object.
     *
     * @param json json
     * @return Java object
     */
    @Override
    public Object fromJson(String json) {
        return parser.get().parse(json);
    }

    /**
     * Convert from json string using Class as a suggestion for how to do the parse.
     *
     * @param json json
     * @param cls  cls
     * @param <T>  Type
     * @return Java object of Type T
     */
    @Override
    public <T> T fromJson(String json, Class<T> cls) {
        return parser.get().parse(cls, json);
    }

    /**
     * Converts from a json string using componentClass as a guide to a List.
     *
     * @param json           json
     * @param componentClass componentClass
     * @param <T>            Type
     * @return List of Java objects of Type T.
     */
    @Override
    public <T> List<T> fromJsonArray(String json, Class<T> componentClass) {
        return parser.get().parseList(componentClass, json);
    }

    /**
     * Converts from Object into JSON string.
     *
     * @param object object to convert to JSON.
     * @return json string
     */
    @Override
    public String toJson(Object object) {
        return serializer.get().serialize(object).toString();
    }

    /**
     * Converts from a json string using componentClassKey and componentClassValue as a guide to a Map.
     *
     * @param json                json string
     * @param componentClassKey   componentClassKey type of Key
     * @param componentClassValue componentClassValue type of value
     * @param <K>                 K type of map key
     * @param <V>                 V type of map value
     * @return Map
     */
    @Override
    public <K, V> Map<K, V> fromJsonMap(String json, Class<K> componentClassKey, Class<V> componentClassValue) {
        Map map = (Map) parser.get().parse(json);
        final Mapper mapper = this.mapper.get();
        return extractMap(componentClassKey, componentClassValue, map, mapper);
    }

    private <K, V> Map<K, V> extractMap(Class<K> componentClassKey, Class<V> componentClassValue, Map<Object, Object> map, Mapper mapper) {
        final Map<K, V> results = new TreeMap<>();

        /* Convert each entry give the componentClassKey and the componentClassValue. */
        map.entrySet().forEach(entry -> {

            /** value. */
            final Object value = entry.getValue() instanceof ValueContainer ? ((ValueContainer) entry.getValue()).toValue() : entry.getValue();
            /** key */
            final Object key = entry.getKey() instanceof ValueContainer ? ((ValueContainer) entry.getKey()).toValue() : entry.getKey();
            /** Converted key. */
            final K convertedKey;
            /** Converted value. */
            final V convertedValue;

            /** If the key is a map then convert it into the type with the mapper. */
            if (key instanceof Map) {
                convertedKey = mapper.fromMap(((Map<String, Object>) key), componentClassKey);
            } else {

                /** If the key is not a map then convert it into the type with Conversions. */
                convertedKey = Conversions.coerce(componentClassKey, key);
            }

            if (value instanceof Map) {

                /** If the value is a map use the mapper to convert to an object unless the
                 * componentClassValue is Object then just convert to a map of basic types.
                 */
                if (!(componentClassValue == Object.class)) {
                    convertedValue = mapper.fromMap(((Map<String, Object>) value), componentClassValue);
                } else {
                    /**
                     * componentClassValue is Object to convert it to a regular map.
                     */
                    if (value instanceof ValueMap) {
                        convertedValue = convertToMap((ValueMap) value);
                    } else {
                        convertedValue = (V) value;
                    }
                }
            } else if (value instanceof List) {

                convertedValue = (V) convertList(value, mapper);

            } else {
                /** We are not a map so just convert normally. */
                if (!(componentClassValue == Object.class)) {
                    convertedValue = Conversions.coerce(componentClassValue, value);
                } else {
                    /** Unless componentClassValue is Object then we want to pull out the values. */
                    if (value instanceof Value) {
                        convertedValue = (V) ((Value) value).toValue();
                    } else {
                        convertedValue = (V) value;
                    }
                }
            }

            results.put(convertedKey, convertedValue);

        });

        return results;
    }

    private Object convertList(Object value, Mapper mapper) {

        final List list = (List) value;
        final Object convertedValue;
        if (list.size() == 0) {
            convertedValue = list;
        } else {
            final List convertedList = new ArrayList(list.size());

            list.forEach(item -> {

                final Object itemValue;

                if (item instanceof ValueContainer) {
                    itemValue = ((ValueContainer) item).toValue();
                } else {
                    itemValue = item;
                }

                final Object newItemValue;
                /** We are not a map so just convert normally. */
                if (itemValue instanceof List) {
                    newItemValue = convertList(itemValue, mapper);
                } else if (itemValue instanceof Map) {
                    Map m = ((Map) itemValue);

                    if (m instanceof ValueMap) {
                        newItemValue = convertToMap(((ValueMap) m));
                    } else {
                        newItemValue = extractMap(String.class, Object.class, m, mapper);
                    }

                } else {
                    /** Unless componentClassValue is Object then we want to pull out the values. */
                    if (itemValue instanceof Value) {
                        newItemValue = ((Value) itemValue).toValue();
                    } else {
                        newItemValue = itemValue;
                    }
                }
                convertedList.add(newItemValue);
            });
            convertedValue = convertedList;
        }
        return convertedValue;
    }

    /**
     * Helper method.
     * Converts a value map into a regular map of Java basic types.
     *
     * @param valueMap valueMap
     * @param <V>      V
     * @return regular map
     */
    private <V> V convertToMap(ValueMap valueMap) {
        final Map<String, Object> map = new LinkedHashMap<>(valueMap.size());

        valueMap.entrySet().forEach(new Consumer<Map.Entry<String, Object>>() {
            @Override
            public void accept(Map.Entry<String, Object> entry) {

                Object value = entry.getValue();

                /* If the value is a value container then grab what is inside. */
                if (value instanceof ValueContainer) {
                    ValueContainer valueContainer = ((ValueContainer) entry.getValue());
                    value = valueContainer.toValue();
                }

                /* If value is a Value then pull the real value. */
                if (value instanceof Value) {
                    map.put(entry.getKey(), ((Value) value).toValue());
                } else if (value instanceof ValueMap) {
                    /* If value is a value map then convert it into a regular map. */
                    map.put(entry.getKey(), convertToMap(((ValueMap) value)));
                } else if (value instanceof List) {
                    map.put(entry.getKey(), convertList(value, mapper.get()));
                } else {
                    map.put(entry.getKey(), value);
                }

            }
        });

        return (V) map;
    }

}


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

import io.advantageous.boon.core.Lists;
import io.advantageous.boon.core.Str;
import io.advantageous.boon.core.Sys;
import io.advantageous.boon.core.reflection.Annotated;
import io.advantageous.boon.core.reflection.AnnotationData;
import io.advantageous.boon.core.reflection.ClassMeta;
import io.advantageous.boon.core.reflection.MethodAccess;
import io.advantageous.qbit.annotation.AnnotationUtils;
import io.advantageous.qbit.annotation.RequestMethod;
import io.advantageous.qbit.message.Event;
import io.advantageous.qbit.message.MethodCall;
import io.advantageous.qbit.message.Response;
import io.advantageous.qbit.message.impl.ResponseImpl;
import io.advantageous.qbit.queue.QueueCallBackHandler;
import io.advantageous.qbit.queue.SendQueue;
import io.advantageous.qbit.reactive.Callback;
import io.advantageous.qbit.service.ServiceMethodHandler;
import io.advantageous.qbit.service.impl.ServiceConstants;
import io.advantageous.reakt.promise.Promise;

import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;

import static io.advantageous.boon.core.Str.sputs;
import static io.advantageous.qbit.annotation.AnnotationUtils.*;

/**
 * created by Richard on 9/8/14.
 *
 * @author Rick Hightower
 */
public class BoonServiceMethodCallHandler implements ServiceMethodHandler {

    final Map<String, Boolean> hasHandlerMap = new HashMap<>();
    private final boolean invokeDynamic;
    private final MapAndInvoke mapAndInvoke;
    private final TreeSet<String> addresses = new TreeSet<>();
    private final Map<String, MethodAccess> methodMap = new LinkedHashMap<>();
    /**
     * Holds on to Boon cache so we don't have to recreate reflected gak.
     */
    private final Object context = Sys.contextToHold();
    private final Map<String, MethodAccess> eventMap = new ConcurrentHashMap<>();
    protected Object service;
    SendQueue<Response<Object>> responseSendQueue;
    private ClassMeta<Class<?>> classMeta;
    private QueueCallBackHandler queueCallBackHandler;
    private String address = "";
    private String name = "";

    public BoonServiceMethodCallHandler(final boolean invokeDynamic) {
        this.invokeDynamic = invokeDynamic;

        if (invokeDynamic) {
            this.mapAndInvoke = new MapAndInvokeDynamic(this);
        } else {
            this.mapAndInvoke = new MapAndInvokeImpl(this);
        }
    }

    @Override
    public Response<Object> receiveMethodCall(MethodCall<Object> methodCall) {

        try {
            return invokeByName(methodCall);
        } catch (Exception ex) {

            if (ex.getCause() instanceof InvocationTargetException) {
                InvocationTargetException tex = (InvocationTargetException) ex.getCause();
                return new ResponseImpl<>(methodCall, tex.getTargetException());
            }
            return new ResponseImpl<>(methodCall, ex);
        }
    }

    private Response<Object> mapArgsAsyncHandlersAndInvoke(MethodCall<Object> methodCall, MethodAccess serviceMethod) {
        return this.mapAndInvoke.mapArgsAsyncHandlersAndInvoke(methodCall, serviceMethod);
    }

    /**
     * False is unknown, true is no callbacks.
     *
     * @param name name of method
     * @return false signifies maybe, true means never.
     */
    public boolean couldHaveCallback(final String name) {

        final Boolean has = hasHandlerMap.get(name);
        if (has == null) {
            return true;
        }

        return has;
    }

    protected boolean hasHandlers(MethodCall<Object> methodCall, MethodAccess serviceMethod) {

        Boolean has = hasHandlerMap.get(methodCall.name());

        boolean hasHandlers;
        if (has == null) {
            hasHandlers = hasHandlers(methodCall);
            hasHandlers = hasHandlers(serviceMethod) || hasHandlers;
            hasHandlerMap.put(methodCall.name(), hasHandlers);
        } else {
            hasHandlers = has;
        }

        return hasHandlers;

    }

    private boolean hasHandlers(MethodAccess serviceMethod) {

        if (Promise.class.isAssignableFrom(serviceMethod.returnType())) {
            return true;
        }

        for (Class<?> paramType : serviceMethod.parameterTypes()) {
            if (paramType == Callback.class) {
                return true;
            }
        }
        return false;
    }

    private boolean hasHandlers(MethodCall<Object> methodCall) {

        if (methodCall.hasCallback()) {
            return true;
        }
        if (methodCall.body() instanceof List) {

            final List body = (List) methodCall.body();
            for (Object item : body) {
                if (item instanceof Callback) {
                    return true;
                }
            }
            return false;
        } else if (methodCall.body() instanceof Object[]) {

            final Object[] body = (Object[]) methodCall.body();
            for (Object item : body) {
                if (item instanceof Callback) {
                    return true;
                }
            }
            return false;

        } else {
            return methodCall.body() instanceof Callback;
        }
    }

    protected void extractHandlersFromArgumentList(Callback<Object> callback, MethodAccess method, Object body, List<Object> argsList) {
        if (body instanceof List) {

            @SuppressWarnings("unchecked") List<Object> list = (List<Object>) body;

            extractHandlersFromArgumentListBodyIsList(callback, method, argsList, list);

        } else if (body instanceof Object[]) {
            extractHandlersFromArgumentListArrayCase(callback, method, (Object[]) body, argsList);
        }
    }

    private void extractHandlersFromArgumentListArrayCase(final Callback<Object> callback,
                                                          final MethodAccess serviceMethod,
                                                          final Object[] inputParams,
                                                          final List<Object> preparedArgumentList) {
        final Class<?>[] parameterTypes = serviceMethod.method().getParameterTypes();

        /** If the argument need by the service method is greater than the arguments than we have in the
         * inputParams than we need to find the callback.
         */
        if (inputParams.length < preparedArgumentList.size()) {
            for (int index = 0, arrayIndex = 0; index < preparedArgumentList.size(); index++, arrayIndex++) {
                final Object o = preparedArgumentList.get(index);
                if (parameterTypes[index] == Callback.class) {
                    if (o == null) {
                        preparedArgumentList.set(index, callback);
                    }
                    arrayIndex--;
                    continue;
                }
                if (arrayIndex >= inputParams.length) {
                    break;
                }
                preparedArgumentList.set(index, inputParams[arrayIndex]);

            }
        } else {

            if (inputParams.length > preparedArgumentList.size()) {
                for (int index = 0, arrayIndex = 0; index < preparedArgumentList.size(); index++, arrayIndex++) {

                    final Object in = inputParams[arrayIndex];
                    if (in instanceof Callback) {
                        arrayIndex++;
                    }
                    if (arrayIndex >= inputParams.length) {
                        break;
                    }
                    preparedArgumentList.set(index, inputParams[arrayIndex]);

                }

            } else {

                for (int index = 0, arrayIndex = 0; index < preparedArgumentList.size(); index++, arrayIndex++) {
                    final Object paramPrepare = preparedArgumentList.get(index);
                    if (paramPrepare instanceof Callback) {
                        continue;
                    }
                    if (arrayIndex >= inputParams.length) {
                        break;
                    }
                    preparedArgumentList.set(index, inputParams[arrayIndex]);

                }
            }
        }

    }

    private void extractHandlersFromArgumentListBodyIsList(final Callback<Object> callback,
                                                           final MethodAccess serviceMethod,
                                                           final List<Object> argsList,
                                                           List<Object> list) {

        final Class<?>[] parameterTypes = serviceMethod.method().getParameterTypes();

        /** Check to see if the array size is one less that the argument size
         * If it is then we think this is a method that has a Callback
         * as the first argument.
         */
        if (serviceMethod.method().getReturnType() == void.class && (list.size() - 1 == serviceMethod.parameterTypes().length)) {
            if (parameterTypes[0] == Callback.class) {
                list = Lists.slc(list, 1); //remove the callback from the args bc the callback is called
                // when the method returns.
            }
        }

        if (list.size() < argsList.size()) {
            for (int index = 0, arrayIndex = 0; index < argsList.size(); index++, arrayIndex++) {
                final Object o = argsList.get(index);
                if (parameterTypes[index] == Callback.class) {
                    if (o == null) {
                        argsList.set(index, callback);
                    }
                    arrayIndex--;
                    continue;
                }
                if (arrayIndex >= list.size()) {
                    break;
                }
                argsList.set(index, list.get(arrayIndex));

            }
        } else {

            final Iterator<Object> iterator = list.iterator();

            for (int index = 0; index < argsList.size(); index++) {

                final Object o = argsList.get(index);
                if (o instanceof Callback) {
                    continue;
                }

                if (!iterator.hasNext()) {
                    break;
                }

                argsList.set(index, iterator.next());
            }
        }
    }

    protected Response<Object> response(MethodAccess methodAccess, MethodCall<Object> methodCall, Object returnValue) {

        if (methodAccess.returnType() == void.class || methodAccess.returnType() == Void.class) {
            return ServiceConstants.VOID;
        }
        return ResponseImpl.response(methodCall.id(), methodCall.timestamp(), methodCall.address(), methodCall.returnAddress(), returnValue, methodCall);
    }

    protected List<Object> prepareArgumentList(final MethodCall<Object> methodCall, Class<?>[] parameterTypes) {
        final List<Object> argsList = new ArrayList<>(parameterTypes.length);
        for (Class<?> parameterType : parameterTypes) {
            if (parameterType == Callback.class) {
                argsList.add(createCallBackHandler(methodCall));
                continue;
            }
            argsList.add(null);
        }
        return argsList;

    }

    protected Callback<Object> createCallBackHandler(final MethodCall<Object> methodCall) {

        return new BoonCallBackWrapper(responseSendQueue, methodCall);

    }

    private Response<Object> invokeByName(MethodCall<Object> methodCall) {
        final MethodAccess method = classMeta.method(methodCall.name());

        if (method != null) {
            return mapArgsAsyncHandlersAndInvoke(methodCall, method);
        } else {

            if (methodCall.name().equals("toString")) {
                return ResponseImpl.response(
                        methodCall.id(),
                        methodCall.timestamp(),
                        methodCall.address(),
                        methodCall.returnAddress(),
                        sputs("Method Call toString was called", methodCall.objectName(), methodCall.name(), methodCall.address()),
                        methodCall, false);

            } else {
                return ResponseImpl.response(
                        methodCall.id(),
                        methodCall.timestamp(),
                        methodCall.address(),
                        methodCall.returnAddress(),
                        new Exception("Unable to find method " + methodCall.name()),
                        methodCall, true);
            }
        }
    }

    @Override
    public void init(
            Object service,
            String rootAddress,
            String serviceAddress,
            SendQueue<Response<Object>> responseSendQueue) {

        this.responseSendQueue = responseSendQueue;
        this.service = service;
        //noinspection unchecked
        classMeta = (ClassMeta<Class<?>>) ClassMeta.classMeta(service.getClass());

        addresses.add(Str.add(rootAddress, "/", classMeta.longName()));
        addresses.add(Str.add(rootAddress, "/", classMeta.longName().toLowerCase()));

        if (Str.isEmpty(serviceAddress)) {
            serviceAddress = readAddressFromAnnotation(classMeta);
        }
        if (Str.isEmpty(serviceAddress)) {

            serviceAddress = Str.camelCaseLower(classMeta.name());
        }

        this.name = readNameFromAnnotation(classMeta);

        if (Str.isEmpty(name)) {

            this.name = Str.uncapitalize(classMeta.name());

        }

        if (serviceAddress.endsWith("/")) {
            serviceAddress = Str.slc(serviceAddress, 0, -1);
        }

        if (!Str.isEmpty(rootAddress)) {

            if (rootAddress.endsWith("/")) {
                rootAddress = Str.slc(rootAddress, 0, -1);
            }

            if (serviceAddress.startsWith("/")) {
                this.address = Str.add(rootAddress, serviceAddress);
            } else {
                this.address = Str.add(rootAddress, "/", serviceAddress);

            }
        } else {
            this.address = serviceAddress;
        }

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

        for (MethodAccess methodAccess : methods) {
            final AnnotationData listen = getListenAnnotation(methodAccess);
            if (listen == null) {
                continue;
            }
            String channel = listen.getValues().get("value").toString();
            eventMap.put(channel, methodAccess);
        }

        final Class<?>[] interfaces = classMeta.cls().getInterfaces();
        for (Class<?> interfaceClass : interfaces) {

            if (interfaceClass.getName().equals("groovy.lang.GroovyObject")) continue;

            final ClassMeta<?> interfaceMeta = ClassMeta.classMeta(interfaceClass);

            final AnnotationData channelAnnotation =
                    interfaceMeta.annotation(AnnotationUtils.EVENT_CHANNEL_ANNOTATION_NAME);

            if (channelAnnotation == null) {
                continue;
            }

            final String classEventBusName = getClassEventChannelName(interfaceMeta, channelAnnotation);

            interfaceMeta.methods().forEach(methodAccess -> {

                AnnotationData methodAnnotation = methodAccess.annotation(AnnotationUtils.EVENT_CHANNEL_ANNOTATION_NAME);

                String methodEventBusName = methodAnnotation != null && methodAnnotation.getValues().get("value") != null
                        ? methodAnnotation.getValues().get("value").toString() : null;

                if (Str.isEmpty(methodEventBusName)) {
                    methodEventBusName = methodAccess.name();
                }

                final String channelName = createChannelName(null, classEventBusName, methodEventBusName);
                eventMap.put(channelName, methodAccess);

            });
        }

        readMethodMetaData();
        initQueueHandlerMethods();

    }

    private void initQueueHandlerMethods() {

        this.queueCallBackHandler = QueueCallbackHandlerFactory.createQueueCallbackHandler(service);

    }

    @Override
    public void init() {
        queueCallBackHandler.queueInit();
    }

    @Override
    public void startBatch() {
        queueCallBackHandler.queueStartBatch();

    }

    private void readMethodMetaData() {

        final Iterable<MethodAccess> methods = classMeta.methods();
        for (MethodAccess methodAccess : methods) {

            registerMethod(methodAccess);
        }

        addresses.addAll(methodMap.keySet());
    }

    private void registerMethod(MethodAccess methodAccess) {

        if (!methodAccess.isPrivate()) {

            String methodAddress = readAddressFromAnnotation(methodAccess);

            if (methodAddress != null && !methodAddress.isEmpty()) {

                doRegisterMethodUnderURI(methodAccess, methodAddress);

            }

        }

        doRegisterMethodUnderURI(methodAccess, methodAccess.name());
        doRegisterMethodUnderURI(methodAccess, methodAccess.name().toLowerCase());
        doRegisterMethodUnderURI(methodAccess, methodAccess.name().toUpperCase());

    }

    private void doRegisterMethodUnderURI(final MethodAccess methodAccess, String methodAddress) {

        if (methodAddress.startsWith("/")) {
            methodAddress = Str.slc(methodAddress, 1);
        }

        this.methodMap.put(Str.add(this.address, "/", methodAddress), methodAccess);

    }

    @Override
    public TreeSet<String> addresses() {
        return addresses;
    }

    @Override
    public void handleEvent(Event<Object> event) {

        MethodAccess methodAccess = eventMap.get(event.channel());

        if (invokeDynamic) {
            final Object body = event.body();

            if (body instanceof List) {
                List list = ((List) body);
                methodAccess.invokeDynamic(service, list.toArray(new Object[list.size()]));

            } else if (body instanceof Object[]) {
                final Object[] array = (Object[]) body;
                methodAccess.invokeDynamic(service, array);

            } else {
                methodAccess.invokeDynamicObject(service, body);
            }
        } else {
            final Object body = event.body();

            if (body instanceof List) {
                List list = ((List) body);
                methodAccess.invoke(service, list.toArray(new Object[list.size()]));

            } else if (body instanceof Object[]) {
                final Object[] array = (Object[]) body;
                methodAccess.invoke(service, array);

            } else {
                methodAccess.invoke(service, body);
            }
        }
    }

    private String readAddressFromAnnotation(Annotated annotated) {
        String address = getAddress("RequestMapping", annotated);

        if (Str.isEmpty(address)) {
            address = getAddress("Name", annotated);
        }

        if (Str.isEmpty(address)) {
            address = getAddress("Service", annotated);
        }

        if (Str.isEmpty(address)) {
            address = getAddress("ServiceMethod", annotated);
        }

        return address == null ? "" : address;
    }

    private String readNameFromAnnotation(Annotated annotated) {
        String name = getAddress("Named", annotated);

        if (Str.isEmpty(name)) {
            name = getAddress("Name", annotated);
        }

        if (Str.isEmpty(name)) {
            name = getAddress("Service", annotated);
        }

        if (Str.isEmpty(name)) {
            name = getAddress("ServiceName", annotated);
        }

        return name == null ? "" : name;
    }

    private String getHttpMethod(@SuppressWarnings("SameParameterValue") String name, Annotated annotated) {
        AnnotationData requestMapping = annotated.annotation(name);

        if (requestMapping != null) {
            Object value = requestMapping.getValues().get("method");

            if (value instanceof String[]) {

                String[] values = (String[]) value;
                if (values.length > 0 && values[0] != null && !values[0].isEmpty()) {
                    return values[0];
                }
            }
            if (value instanceof RequestMethod[]) {

                RequestMethod[] values = (RequestMethod[]) value;
                if (values.length > 0 && values[0] != null) {
                    return values[0].toString();
                }
            } else {

                return value.toString();
            }
        }
        return null;
    }

    private String getAddress(String name, Annotated annotated) {
        AnnotationData requestMapping = annotated.annotation(name);

        if (requestMapping != null) {
            Object value = requestMapping.getValues().get("value");

            if (value instanceof String[]) {

                String[] values = (String[]) value;
                if (values.length > 0 && values[0] != null && !values[0].isEmpty()) {
                    return values[0];
                }
            } else {

                return (String) value;
            }
        }
        return null;
    }

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

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

    @Override
    public void receive(MethodCall<Object> item) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void empty() {

        queueCallBackHandler.queueEmpty();

    }

    @Override
    public void limit() {
        queueCallBackHandler.queueLimit();
    }

    @Override
    public void shutdown() {
        queueCallBackHandler.queueShutdown();
    }

    @Override
    public void idle() {
        queueCallBackHandler.queueIdle();
    }

    static class BoonCallBackWrapper implements Callback<Object> {
        final SendQueue<Response<Object>> responseSendQueue;
        final MethodCall<Object> methodCall;

        BoonCallBackWrapper(final SendQueue<Response<Object>> responseSendQueue, final MethodCall<Object> methodCall) {

            this.responseSendQueue = responseSendQueue;
            this.methodCall = methodCall;
        }

        @Override
        public void accept(Object result) {
            responseSendQueue.send(ResponseImpl.response(methodCall, result));
        }

        @Override
        public void onError(Throwable error) {
            responseSendQueue.send(ResponseImpl.error(methodCall, error));
        }

        @Override
        public void onTimeout() {
            responseSendQueue.send(ResponseImpl.error(methodCall, new TimeoutException("Method call " + methodCall.name() + " timed out ")));
        }
    }

}


package io.advantageous.qbit.reactive;

import org.slf4j.Logger;

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

/**
 * You need this is you want to do error handling (Exception) from a callback.
 * Callback Builder
 * created by rhightower on 3/23/15.
 */
@SuppressWarnings("UnusedReturnValue")
public class CallbackBuilder {

    private Reactor reactor;
    private Callback callback;
    private Runnable onTimeout;
    private long timeoutDuration = -1;
    private TimeUnit timeoutTimeUnit = TimeUnit.SECONDS;
    private Consumer<Throwable> onError;
    private boolean supportLatch;

    /**
     * @param reactor reactor
     */
    private CallbackBuilder(final Reactor reactor) {
        this.reactor = reactor;
    }

    /**
     *
     */
    private CallbackBuilder() {
    }

    /**
     * @param reactor reactor
     * @return CallbackBuilder
     */
    public static CallbackBuilder callbackBuilder(final Reactor reactor) {
        return new CallbackBuilder(reactor);
    }

    /**
     * Creating callback builder.
     *
     * @param reactor reactor
     * @return CallbackBuilder
     */
    public static CallbackBuilder newCallbackBuilderWithReactor(final Reactor reactor) {
        return new CallbackBuilder(reactor);
    }

    /**
     * @return CallbackBuilder
     */
    public static CallbackBuilder callbackBuilder() {
        return new CallbackBuilder();
    }

    /**
     * @return CallbackBuilder
     */
    public static CallbackBuilder newCallbackBuilder() {
        return new CallbackBuilder();
    }

    /**
     * @return Reactor
     */
    public Reactor getReactor() {
        return reactor;
    }

    /**
     * @param <T> T type of callback.
     * @return callback
     */
    public <T> Callback<T> getCallback() {
        //noinspection unchecked
        return callback;
    }

    /**
     * Builder method to add a callback handler.
     *
     * @param callback callback
     * @return this
     */
    public CallbackBuilder setCallback(final Callback callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to add a callback handler.
     *
     * @param returnType returnType
     * @param callback   callback
     * @param <T>        T
     * @return this
     */
    public <T> CallbackBuilder setCallback(final Class<T> returnType, final Callback<T> callback) {
        return withCallback(returnType, callback);
    }

    /**
     * Builder method to set the callback handler.
     *
     * @param returnType returnType
     * @param callback   callback
     * @param <T>        T
     * @return this
     */
    public <T> CallbackBuilder withCallback(final Class<T> returnType,
                                            final Callback<T> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to delegate timeout and error handling to other callback.
     *
     * @param callback callback
     * @param <T>      T
     * @return this
     */
    public <T> CallbackBuilder delegate(final Callback<T> callback) {

        this.withErrorHandler(callback::onError);

        this.withTimeoutHandler(callback::onTimeout);

        return this;
    }

    /**
     * Builder method to wrap and delegate, timeout and error handling and callback itself.
     *
     * @param callback callback
     * @param <T>      T
     * @return this
     */
    public <T> CallbackBuilder wrap(final Callback<T> callback) {

        this.withErrorHandler(callback::onError);

        this.withTimeoutHandler(callback::onTimeout);

        this.withCallback(callback);

        return this;
    }

    /**
     * Builder method to delegate timeout and error handling to other callback.
     *
     * @param callback      callback
     * @param logger        logger
     * @param operationName method name or thing you are doing.
     * @param <T>           T
     * @return this, fluent
     */
    public <T> CallbackBuilder delegateWithLogging(final Callback<T> callback, final Logger logger,
                                                   final String operationName) {

        this.withErrorHandler(throwable -> {
            logger.error(operationName + " ERROR ", throwable);
            callback.onError(throwable);
        });

        this.withTimeoutHandler(() -> {
            logger.error(operationName + " TIMED OUT ");
            callback.onTimeout();
        });

        return this;
    }

    /**
     * Builder method to wrap / delegate timeout and error handling as well as callback itself.
     *
     * @param callback      callback
     * @param logger        logger
     * @param operationName method name or thing you are doing.
     * @param <T>           T
     * @return this, fluent
     */
    public <T> CallbackBuilder wrapWithLogging(final Callback<T> callback, final Logger logger,
                                               final String operationName) {

        this.withErrorHandler(throwable -> {
            logger.error(operationName + " error ", throwable);
            callback.onError(throwable);
        });

        this.withTimeoutHandler(() -> {
            logger.error(operationName + " TIMED OUT ");
            callback.onTimeout();
        });

        this.withCallback(callback);
        return this;
    }

    /**
     * Builder method to set the callback handler.
     *
     * @param callback callback
     * @param <T>      T
     * @return callback
     */
    public <T> CallbackBuilder withCallback(final Callback<T> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes a list
     *
     * @param componentClass componentClass
     * @param callback       callback
     * @param <T>            T
     * @return this
     */
    public <T> CallbackBuilder withListCallback(final Class<T> componentClass,
                                                final Callback<List<T>> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes a set
     *
     * @param componentClass componentClass
     * @param callback       callback
     * @param <T>            T
     * @return this
     */
    public <T> CallbackBuilder withSetCallback(final Class<T> componentClass,
                                               final Callback<Set<T>> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes a collection
     *
     * @param componentClass componentClass
     * @param callback       callback
     * @param <T>            T
     * @return this
     */
    public <T> CallbackBuilder withCollectionCallback(final Class<T> componentClass,
                                                      final Callback<Collection<T>> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes a map
     *
     * @param keyClass   keyClass
     * @param valueClass valueClass
     * @param callback   callback
     * @param <K>        key type
     * @param <V>        value type
     * @return this
     */
    public <K, V> CallbackBuilder withMapCallback(final Class<K> keyClass,
                                                  final Class<V> valueClass,
                                                  final Callback<Map<K, V>> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes a boolean
     *
     * @param callback callback
     * @return this
     */
    public CallbackBuilder withBooleanCallback(final Callback<Boolean> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes a integer
     *
     * @param callback callback
     * @return this
     */
    public CallbackBuilder withIntCallback(final Callback<Integer> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes a long
     *
     * @param callback callback
     * @return this
     */
    public CallbackBuilder withLongCallback(final Callback<Long> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes a string
     *
     * @param callback callback
     * @return this
     */
    public CallbackBuilder withStringCallback(final Callback<String> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes an optional string
     *
     * @param callback callback
     * @return this
     */
    public CallbackBuilder withOptionalStringCallback(final Callback<Optional<String>> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * Builder method to set callback handler that takes an optional string
     *
     * @param cls      class
     * @param callback callback
     * @param <T>      type of callback
     * @return this
     */
    public <T> CallbackBuilder withOptionalCallback(final Class<T> cls, final Callback<Optional<T>> callback) {
        this.callback = callback;
        return this;
    }

    /**
     * @return runnable
     */
    public Runnable getOnTimeout() {
        return onTimeout;
    }

    /**
     * @param onTimeout onTimeout
     * @return this
     */
    public CallbackBuilder setOnTimeout(final Runnable onTimeout) {
        this.onTimeout = onTimeout;
        return this;
    }

    /**
     * Add a timeout handler to the callback.
     *
     * @param timeoutHandler timeoutHandler
     * @return this
     */
    public CallbackBuilder withTimeoutHandler(final Runnable timeoutHandler) {
        this.onTimeout = timeoutHandler;
        return this;
    }

    /**
     * @return timeout duration
     */
    public long getTimeoutDuration() {
        return timeoutDuration;
    }

    /**
     * @param timeoutDuration timeoutDuration
     * @return this
     */
    public CallbackBuilder setTimeoutDuration(@SuppressWarnings("SameParameterValue") long timeoutDuration) {
        this.timeoutDuration = timeoutDuration;
        return this;
    }

    /**
     * @param timeoutDuration timeoutDuration
     * @return this
     */
    public CallbackBuilder withTimeout(@SuppressWarnings("SameParameterValue") long timeoutDuration) {
        this.timeoutDuration = timeoutDuration;
        return this;
    }

    /**
     * @return time unit
     */
    public TimeUnit getTimeoutTimeUnit() {
        return timeoutTimeUnit;
    }

    /**
     * @param timeoutTimeUnit timeoutTimeUnit
     * @return this
     */
    public CallbackBuilder setTimeoutTimeUnit(final TimeUnit timeoutTimeUnit) {
        this.timeoutTimeUnit = timeoutTimeUnit;
        return this;
    }

    /**
     * @param timeoutTimeUnit timeoutTimeUnit
     * @return this
     */
    public CallbackBuilder withTimeoutTimeUnit(final TimeUnit timeoutTimeUnit) {
        this.timeoutTimeUnit = timeoutTimeUnit;
        return this;
    }

    /**
     * @return error handler
     */
    public Consumer<Throwable> getOnError() {
        return onError;
    }

    /**
     * Sets onError handler.
     *
     * @param onError on error handler.
     * @return this
     */
    public CallbackBuilder setOnError(Consumer<Throwable> onError) {
        this.onError = onError;
        return this;
    }

    /**
     * Add an error handler to the callback.
     *
     * @param onError onerror
     * @return this
     */
    public CallbackBuilder withErrorHandler(final Consumer<Throwable> onError) {
        this.onError = onError;
        return this;
    }

    public <T> AsyncFutureCallback<T> build() {

        if (getOnError() != null || getOnTimeout() != null) {

            if (timeoutDuration == -1) {
                timeoutDuration = 30;
            }

            if (reactor != null) {

                if (this.isSupportLatch()) {
                    return reactor.callbackWithTimeoutAndErrorHandlerAndOnTimeoutWithLatch(
                            (Callback<T>) getCallback(),
                            getTimeoutDuration(),
                            getTimeoutTimeUnit(),
                            getOnTimeout(),
                            getOnError());

                } else {
                    //noinspection unchecked
                    return reactor.callbackWithTimeoutAndErrorHandlerAndOnTimeout(
                            (Callback<T>) getCallback(),
                            getTimeoutDuration(),
                            getTimeoutTimeUnit(),
                            getOnTimeout(),
                            getOnError());
                }
            } else {
                return new AsyncFutureCallback<T>() {

                    @Override
                    public boolean checkTimeOut(long now) {

                        throw new IllegalStateException("You need to register a reactor to use this feature");
                    }

                    @Override
                    public void run() {
                        throw new IllegalStateException("You need to register a reactor to use this feature");

                    }

                    @Override
                    public boolean cancel(boolean mayInterruptIfRunning) {
                        throw new IllegalStateException("You need to register a reactor to use this feature");
                    }

                    @Override
                    public boolean isCancelled() {
                        throw new IllegalStateException("You need to register a reactor to use this feature");
                    }

                    @Override
                    public boolean isDone() {
                        throw new IllegalStateException("You need to register a reactor to use this feature");
                    }

                    @Override
                    public T get() {
                        throw new IllegalStateException("You need to register a reactor to use this feature");
                    }

                    @Override
                    public T get(long timeout, TimeUnit unit) {
                        throw new IllegalStateException("You need to register a reactor to use this feature");
                    }

                    @Override
                    public void accept(T t) {
                        getCallback().accept(t);
                    }

                    @Override
                    public void onError(final Throwable error) {

                        getOnError().accept(error);
                    }

                    @Override
                    public void onTimeout() {

                        getOnTimeout().run();
                    }
                };
            }

        }

        if (reactor != null) {
            if (isSupportLatch()) {
                return reactor.callback(this.getCallback());
            } else {
                return reactor.callbackWithLatch(this.getCallback());
            }
        } else {

            final Callback callback = this.getCallback();
            return new AsyncFutureCallback<T>() {
                @Override
                public boolean checkTimeOut(long now) {
                    return false;
                }

                @Override
                public void accept(T t) {

                    callback.accept(t);
                }

                @Override
                public void onError(Throwable error) {

                    callback.onError(error);
                }

                @Override
                public void run() {

                }

                @Override
                public boolean cancel(boolean mayInterruptIfRunning) {
                    throw new IllegalStateException("You need to register a reactor to use this feature");

                }

                @Override
                public boolean isCancelled() {
                    throw new IllegalStateException("You need to register a reactor to use this feature");
                }

                @Override
                public boolean isDone() {
                    throw new IllegalStateException("You need to register a reactor to use this feature");
                }

                @Override
                public T get() {
                    throw new IllegalStateException("You need to register a reactor to use this feature");
                }

                @Override
                public T get(long timeout, TimeUnit unit) {
                    throw new IllegalStateException("You need to register a reactor to use this feature");
                }
            };
        }
    }

    public <T> AsyncFutureCallback<T> build(Class<T> returnType) {

        return build();
    }

    public <T> AsyncFutureCallback<T> build(Callback<T> callback) {

        this.withCallback(callback);

        return build();
    }

    public boolean isSupportLatch() {
        return supportLatch;
    }

    public CallbackBuilder setSupportLatch(boolean supportLatch) {
        this.supportLatch = supportLatch;
        return this;
    }
}


package io.advantageous.qbit.http.request;

import io.advantageous.qbit.http.request.impl.HttpBinaryResponseImpl;
import io.advantageous.qbit.http.request.impl.HttpTextResponseImpl;
import io.advantageous.qbit.util.MultiMap;
import io.advantageous.qbit.util.MultiMapImpl;

import java.nio.charset.StandardCharsets;

/**
 * Builds an HTTP Response.
 */
public class HttpResponseBuilder {

    /**
     * Response code.
     */
    private int code = 200;

    /**
     * Content Type, e.g., "application/json"
     */
    private String contentType;

    /**
     * Response body.
     */
    private Object body;

    /**
     * Response headers.
     */
    private MultiMap<String, String> headers;

    /**
     * Builder creator
     *
     * @return httpResponseBuilder
     */
    public static HttpResponseBuilder httpResponseBuilder() {
        return new HttpResponseBuilder();
    }

    public int getCode() {
        return code;
    }

    public HttpResponseBuilder setCode(final int code) {
        this.code = code;
        return this;
    }

    public String getContentType() {
        return contentType;
    }

    public HttpResponseBuilder setContentType(final String contentType) {
        this.contentType = contentType;
        return this;
    }

    /**
     * Sets a JSON body.
     *
     * @param json json
     * @return this, fluent
     */
    public HttpResponseBuilder setJsonBody(final String json) {
        this.setContentType("application/json");
        this.setBody(json);
        return this;
    }

    /**
     * Sets a JSON body with OK (200) http code.
     *
     * @param json json
     * @return this, fluent
     */
    public HttpResponseBuilder setJsonBodyCodeOk(final String json) {
        this.setCode(200);
        this.setContentType("application/json");
        this.setBody(json);
        return this;
    }

    /**
     * Sets a JSON body with (500) http code.
     *
     * @param json json
     * @return this, fluent
     */
    public HttpResponseBuilder setJsonBodyError(final String json) {
        this.setCode(500);
        this.setContentType("application/json");
        this.setBody(json);
        return this;
    }

    public Object getBody() {
        return body;
    }

    public HttpResponseBuilder setBody(final Object body) {
        this.body = body;
        return this;
    }

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

    public HttpResponseBuilder setHeaders(final MultiMap<String, String> headers) {
        this.headers = headers;
        return this;
    }

    /**
     * Add a header
     *
     * @param name  name
     * @param value value
     * @return HttpResponseBuilder
     */
    public HttpResponseBuilder addHeader(final String name, final String value) {
        getHeaders().put(name, value);
        return this;
    }

    /**
     * Build a response object.
     *
     * @return HttpResponse
     */
    public HttpResponse<?> build() {
        if (getBody() instanceof byte[]) {
            return new HttpBinaryResponseImpl(getCode(), getContentType(), (byte[]) getBody(), headers);
        } else {
            return new HttpTextResponseImpl(getCode(), getContentType(), getBody().toString(), headers);
        }
    }

    /**
     * Build a text response object.
     *
     * @return HttpResponse
     */
    public HttpTextResponse buildTextResponse() {
        if (getBody() instanceof byte[]) {
            return new HttpTextResponseImpl(getCode(), getContentType(),
                    new String((byte[]) getBody(), StandardCharsets.UTF_8), headers);
        } else {
            return new HttpTextResponseImpl(getCode(), getContentType(), getBody().toString(), headers);
        }
    }

    /**
     * Build a binary response object.
     *
     * @return HttpResponse
     */
    public HttpBinaryResponse buildBinaryResponse() {
        if (getBody() instanceof byte[]) {
            return new HttpBinaryResponseImpl(getCode(), getContentType(), (byte[]) getBody(), headers);
        } else {
            return new HttpBinaryResponseImpl(getCode(), getContentType(),
                    getBody().toString().getBytes(StandardCharsets.UTF_8), headers);
        }
    }
}


/*
 * Copyright (c) 2015. Rick Hightower, Geoff Chandler
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  		http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * QBit - The Microservice lib for Java : JSON, WebSocket, REST. Be The Web!
 *
 * 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.boon.json.JsonParserAndMapper;
import io.advantageous.boon.json.JsonParserFactory;
import io.advantageous.consul.domain.AgentInfo;
import io.advantageous.consul.domain.Check;
import io.advantageous.consul.domain.HealthCheck;
import io.advantageous.consul.domain.Member;
import io.advantageous.consul.domain.NotRegisteredException;
import io.advantageous.consul.domain.Registration;
import io.advantageous.consul.domain.RegistrationCheck;
import io.advantageous.consul.domain.Service;
import io.advantageous.consul.domain.Status;
import io.advantageous.qbit.http.HTTP;
import io.advantageous.qbit.json.JsonMapper;

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

import static io.advantageous.boon.core.reflection.MapObjectConversion.fromMap;
import static io.advantageous.consul.domain.ConsulException.die;
import static io.advantageous.consul.domain.NotRegisteredException.notRegistered;

/**
 *

 * HTTP Client for Consul agent HTTP API.
 * This is under the path: /v1/agent/ endpoints
 *

 *

 * Note this class was heavily influenced and inspired by the Orbitz Consul client.
 *
 * @see The Consul API Docs
 */
@SuppressWarnings("WeakerAccess")
public class AgentEndpoint extends Endpoint {

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

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

    /**
     * Checks to see if a service is registered with the local agent..
     *
     * @param serviceId service id
     * @return true if a particular service is registered with
     * the local Consul agent, otherwise false.
     */
    public boolean isServiceRegistered(final String serviceId) {
        Map<String, Service> serviceIdToService = getServices();
        return serviceIdToService.containsKey(serviceId);
    }

    /**
     * Pings the Consul Agent.
     */
    public void pingAgent() {

        HTTP.Response response = HTTP.getResponse(createURI("/self").toString());

        if (response.status() != 200) {
            die("Error pinging Consul", response.payloadAsString());
        }
    }

    /**
     * Registers the client as a service with Consul.  Registration enables
     * the use of checks.
     *
     * @param port        The public facing port of the service to register with Consul.
     * @param ttl         Time to live for the Consul dead man's switch.
     * @param serviceName Service name to register.
     * @param serviceId   Service id to register.
     * @param tags        Tags to register with.
     */
    public void registerService(final int port, final long ttl,
                                final String serviceName, final String serviceId,
                                final String... tags) {
        registerService(null, port, ttl, serviceName, serviceId, tags);
    }

    /**
     * Registers the client as a service with Consul.  Registration enables
     * the use of checks.
     *
     * @param host        The public facing host of the service to register with Consul.
     * @param port        The public facing port of the service to register with Consul.
     * @param ttl         Time to live for the Consul dead man's switch.
     * @param serviceName Service name to register.
     * @param serviceId   Service id to register.
     * @param tags        Tags to register with.
     */
    public void registerService(final String host, final int port, final long ttl,
                                final String serviceName, final String serviceId,
                                final String... tags) {
        RegistrationCheck check = new RegistrationCheck();
        check.setTtl("" + ttl + "s");
        registerServiceWithRegistrationCheck(host, port, check, serviceName, serviceId, tags);
    }

    /**
     * Registers the client as a service with Consul.
     * Registration enables
     * the use of check with a script.
     *
     * @param port        The public facing port of the service to register with Consul.
     * @param script      Health script for Consul to use.
     * @param interval    Health script run interval in seconds.
     * @param serviceName Service name to register.
     * @param serviceId   Service id to register.
     * @param tags        Tags to register with.
     */
    public void registerServiceWithScript(final int port,
                                          final String script,
                                          final long interval,
                                          final String serviceName,
                                          final String serviceId,
                                          final String... tags) {
        registerServiceWithScript(null, port, script, interval, serviceName, serviceId, tags);
    }

    /**
     * Registers the client as a service with Consul.
     * Registration enables
     * the use of check with a script.
     *
     * @param host        The public facing host of the service to register with Consul.
     * @param port        The public facing port of the service to register with Consul.
     * @param script      Health script for Consul to use.
     * @param interval    Health script run interval in seconds.
     * @param serviceName Service name to register.
     * @param serviceId   Service id to register.
     * @param tags        Tags to register with.
     */
    public void registerServiceWithScript(final String host,
                                          final int port,
                                          final String script,
                                          final long interval,
                                          final String serviceName,
                                          final String serviceId,
                                          final String... tags) {
        RegistrationCheck check = new RegistrationCheck();
        check.setScript(script);
        check.setInterval("" + interval + "s");
        registerServiceWithRegistrationCheck(host, port, check, serviceName, serviceId, tags);
    }

    /**
     * Registers the client as a service with Consul.  Registration enables
     * the use of checks.
     *
     * @param port  The public facing port of the service to register with Consul.
     * @param check The health check to run periodically.  Can be null.
     * @param name  Service name to register.
     * @param id    Service id to register.
     * @param tags  Tags to register with.
     */
    public void registerServiceWithRegistrationCheck(String host, int port, RegistrationCheck check, String name, String id, String... tags) {
        Registration registration = new Registration();
        registration.setHost(host);
        registration.setPort(port);
        registration.setCheck(check);
        registration.setName(name);
        registration.setId(id);
        registration.setTags(tags);
        register(registration);
    }

    /**
     * Register a service with Consul.
     *
     * @param registration The registration payload.
     */
    public void register(final Registration registration) {

        final URI uri = createURI("/service/register");
        HTTP.Response response = HTTP.jsonRestCallViaPUT(uri.toString(), toJson(registration));

        if (response.status() != 200) {
            die("Error registering service with Consul", uri, registration, response.payloadAsString());
        }
    }

    /**
     * Remove registration of a particular service.
     *
     * @param serviceId the service id that you want to remove.
     */
    public void deregister(final String serviceId) {

        final URI uri = createURI("/service/deregister/" + serviceId);

        HTTP.Response response = HTTP.getResponse(uri.toString());

        if (response.status() != 200) {
            die("Error removing registration of service with Consul",
                    uri, serviceId, response.status(), response.payloadAsString());
        }

    }

    /**
     * Registers a Health Check with the Agent.
     *
     * @param checkId  The Check ID to use.  Must be unique for the Agent.
     * @param name     The Check Name.
     * @param script   Health script for Consul to use.
     * @param interval Health script run interval in seconds.
     */
    public void registerCheck(String checkId, String name, String script, long interval) {
        registerCheckWithNotes(checkId, name, script, interval, null);
    }

    /**
     * Registers a Health Check with the Agent.
     *
     * @param checkId  The Check ID to use.  Must be unique for the Agent.
     * @param name     The Check Name.
     * @param script   Health script for Consul to use.
     * @param interval Health script run interval in seconds.
     * @param notes    Human readable notes.  Not used by Consul.
     */
    public void registerCheckWithNotes(String checkId, String name,
                                       String script, long interval,
                                       @SuppressWarnings("SameParameterValue") String notes) {
        Check check = new Check();
        check.setId(checkId);
        check.setName(name);
        check.setScript(script);
        check.setInterval(String.format("%ss", interval));
        check.setNotes(notes);
        registerCheck(check);
    }

    /**
     * Registers a Health Check with the Agent.
     *
     * @param checkId The Check ID to use.  Must be unique for the Agent.
     * @param name    The Check Name.
     * @param ttl     Time to live for the Consul dead man's switch.
     */
    public void registerCheck(String checkId, String name, long ttl) {
        registerCheck(checkId, name, ttl, null);
    }

    /**
     * Registers a Health Check with the Agent.
     *
     * @param checkId The Check ID to use.  Must be unique for the Agent.
     * @param name    The Check Name.
     * @param ttl     Time to live for the Consul dead man's switch.
     * @param notes   Human readable notes.  Not used by Consul.
     */
    public void registerCheck(String checkId, String name, long ttl, @SuppressWarnings("SameParameterValue") String notes) {
        Check check = new Check();
        check.setId(checkId);
        check.setName(name);
        check.setTtl(String.format("%ss", ttl));
        check.setNotes(notes);
        registerCheck(check);
    }

    /**
     * Registers a Health Check with the Agent.
     *
     * @param check The Check to register.
     */
    public void registerCheck(Check check) {

        final URI uri = createURI("/check/register");

        HTTP.Response response = HTTP.jsonRestCallViaPUT(uri.toString(), toJson(check));

        if (response.status() != 200) {
            die("Error removing registration of service with Consul",
                    uri, check, response.status(), response.statusMessageAsString(),
                    response.payloadAsString());
        }
    }

    /**
     * De-registers a Health Check with the Agent
     *
     * @param checkId the id of the Check to deregister
     */
    public void deregisterCheck(String checkId) {

        final URI uri = createURI("/check/deregister/" + checkId);

        HTTP.Response response = HTTP.getResponse(uri.toString());

        if (response.status() != 200) {
            die("Error removing registration of service with Consul",
                    uri, checkId, response.status(), response.statusMessageAsString(),
                    response.payloadAsString());
        }

    }

    /**
     *

     * Retrieves the Agent's configuration and member information.
     *

     * GET /v1/agent/self
     *
     * @return The Agent information.
     */
    public AgentInfo getAgentInfo() {

        final URI uri = createURI("/self");

        HTTP.Response response = HTTP.getResponse(uri.toString());

        if (response.status() != 200) {
            die("Error getting info about this agent",
                    uri, response.status(), response.statusMessageAsString(),
                    response.payloadAsString());
        }

        return fromJson(response.payloadAsString(), AgentInfo.class);

    }

    /**
     * Retrieves all checks registered with the Agent.
     *

     * GET /v1/agent/checks
     *
     * @return Map of Check ID to Checks.
     */
    public Map<String, HealthCheck> getChecks() {

        final URI uri = createURI("/checks");

        final HTTP.Response response = HTTP.getResponse(uri.toString());

        final JsonParserAndMapper jsonParserAndMapper = new JsonParserFactory().create();
        if (response.status() == 200) {
            final Map<String, Object> map = jsonParserAndMapper.parseMap(response.payloadAsString());
            final Map<String, HealthCheck> returnMap = new HashMap<>(map.size());
            map.entrySet().forEach(entry -> {
                @SuppressWarnings("unchecked") HealthCheck healthCheck = fromMap((Map<String, Object>) entry.getValue(), HealthCheck.class);
                returnMap.put(entry.getKey(), healthCheck);

            });
            return returnMap;
        }
        die("Unable to get health checks", uri, response.status(), response.statusMessageAsString(),
                response.payloadAsString());
        return null;
    }

    /**
     * Retrieves all services registered with the Agent.
     *

     * GET /v1/agent/services
     *
     * @return Map of Service ID to Services.
     */
    public Map<String, Service> getServices() {

        final URI uri = createURI("/services");

        final HTTP.Response response = HTTP.getResponse(uri.toString());

        final JsonParserAndMapper jsonParserAndMapper = new JsonParserFactory().create();
        if (response.status() == 200) {
            final Map<String, Object> map = jsonParserAndMapper.parseMap(response.payloadAsString());
            final Map<String, Service> returnMap = new HashMap<>(map.size());
            map.entrySet().forEach(entry -> {
                @SuppressWarnings("unchecked") Service service = fromMap((Map<String, Object>) entry.getValue(), Service.class);
                returnMap.put(entry.getKey(), service);

            });
            return returnMap;
        }

        die("Unable to get list of services", uri, response.status(), response.payloadAsString());
        return null;
    }

    /**
     * Retrieves all members that the Agent can see in the gossip pool.
     *

     * GET /v1/agent/members
     *
     * @return List of Members.
     */
    public List<Member> getMembers() {

        final URI uri = createURI("/members");
        final HTTP.Response response = HTTP.getResponse(uri.toString());
        if (response.code() == 200) {
            return fromJsonArray(response.body(), Member.class);
        }
        die("Unable to read members", uri, response.code(), response.body());
        return Collections.emptyList();

    }

    /**
     * GET /v1/agent/force-leave/{node}
     *

     * Instructs the agent to force a node into the "left" state.
     *
     * @param node node
     */
    public void forceLeave(String node) {

        final URI uri = createURI("/force-leave/" + node);
        final HTTP.Response httpResponse = HTTP.getResponse(uri.toString());

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

    /**
     * Checks in with Consul.
     *
     * @param checkId The Check ID to check in.
     * @param status  The current state of the Check.
     * @param note    Any note to associate with the Check.
     */
    public void check(String checkId, Status status, String note) {

        final URI uri = createURI("/check/" + status.getUri() + "/" + checkId);

        final HTTP.Response httpResponse = Str.isEmpty(note) ? HTTP.getResponse(uri.toString()) :
                HTTP.getResponse(uri.toString() + "?note=" + note);

        if (httpResponse.code() != 200) {
            notRegistered("Unable to perform check", uri, httpResponse.code(), httpResponse.statusMessageAsString(),
                    httpResponse.body());
        }

    }

    /**
     * Prepends the default TTL prefix to the serviceId to produce a check id,
     * then delegates to check(String checkId, State state, String note)
     * This method only works with TTL checks that have not been given a custom
     * name.
     *
     * @param serviceId service id
     * @param status    state
     * @param note      note
     */
    public void checkTtl(String serviceId, Status status, String note) {
        check("service:" + serviceId, status, note);
    }

    /**
     * Sets a TTL check to "passing" state
     *
     * @param checkId check id
     */
    public void pass(final String checkId) throws NotRegisteredException {
        checkTtl(checkId, Status.PASS, null);
    }

    /**
     * Sets a TTL check to "passing" state with a note
     *
     * @param checkId check id
     * @param note    note
     */
    public void pass(String checkId, @SuppressWarnings("SameParameterValue") String note) throws NotRegisteredException {
        checkTtl(checkId, Status.PASS, note);
    }

    /**
     * Sets a TTL check to "warning" state.
     *
     * @param checkId check id
     */
    public void warn(String checkId) throws NotRegisteredException {
        checkTtl(checkId, Status.WARN, null);
    }

    /**
     * Sets a TTL check to "warning" state with a note.
     *
     * @param checkId check id
     * @param note    note
     */
    public void warn(String checkId, String note) throws NotRegisteredException {
        checkTtl(checkId, Status.WARN, note);
    }

    /**
     * Sets a TTL check to "critical" state.
     *
     * @param checkId check id
     */
    public void fail(String checkId) throws NotRegisteredException {
        checkTtl(checkId, Status.FAIL, null);
    }

    /**
     * Sets a TTL check to "critical" state with a note.
     *
     * @param checkId check id
     * @param note    note
     */
    public void fail(String checkId, String note) throws NotRegisteredException {
        checkTtl(checkId, Status.FAIL, note);
    }

    /**
     * Sets a TTL check to "critical" state.
     *
     * @param checkId check id
     */
    public void critical(String checkId) throws NotRegisteredException {
        checkTtl(checkId, Status.FAIL, null);
    }

    /**
     * Sets a TTL check to "critical" state with a note.
     *
     * @param checkId check id
     * @param note    note
     */
    public void critical(String checkId, String note) throws NotRegisteredException {
        checkTtl(checkId, Status.FAIL, note);
    }

}