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

import java.util.IllegalFormatException;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;

public class Logger
{
    private final java.util.logging.Logger logger;

    Logger(java.util.logging.Logger logger)
    {
        this.logger = logger;
    }

    /**
     * Gets a logger named after a class' fully qualified name.
     *
     * @param clazz the class
     * @return the named logger
     */
    public static Logger get(Class<?> clazz)
    {
        return get(clazz.getName());
    }

    /**
     * Gets a named logger
     *
     * @param name the name of the logger
     * @return the named logger
     */
    public static Logger get(String name)
    {
        java.util.logging.Logger logger = java.util.logging.Logger.getLogger(name);
        return new Logger(logger);
    }

    /**
     * Logs a message at DEBUG level.
     *
     * @param exception an exception associated with the debug message being logged
     * @param message a literal message to log
     */
    public void debug(Throwable exception, String message)
    {
        logger.log(FINE, message, exception);
    }

    /**
     * Logs a message at DEBUG level.
     *
     * @param message a literal message to log
     */
    public void debug(String message)
    {
        logger.fine(message);
    }

    /**
     * Logs a message at DEBUG level.
     *

     * Usage example:
     *

     *    logger.debug("value is %s (%d ms)", value, time);
     *

     * If the format string is invalid or the arguments are insufficient, an error will be logged and execution
     * will continue.
     *
     * @param format a format string compatible with String.format()
     * @param args arguments for the format string
     */
    public void debug(String format, Object... args)
    {
        if (logger.isLoggable(FINE)) {
            String message;
            try {
                message = format(format, args);
            }
            catch (IllegalFormatException e) {
                logger.log(SEVERE, illegalFormatMessageFor("DEBUG", format, args), e);
                message = rawMessageFor(format, args);
            }
            logger.fine(message);
        }
    }

    /**
     * Logs a message at DEBUG level.
     *

     * Usage example:
     *

     *    logger.debug(e, "value is %s (%d ms)", value, time);
     *

     * If the format string is invalid or the arguments are insufficient, an error will be logged and execution
     * will continue.
     *
     * @param exception an exception associated with the debug message being logged
     * @param format a format string compatible with String.format()
     * @param args arguments for the format string
     */
    public void debug(Throwable exception, String format, Object... args)
    {
        if (logger.isLoggable(FINE)) {
            String message;
            try {
                message = format(format, args);
            }
            catch (IllegalFormatException e) {
                logger.log(SEVERE, illegalFormatMessageFor("DEBUG", format, args), e);
                message = rawMessageFor(format, args);
            }
            logger.log(FINE, message, exception);
        }
    }

    /**
     * Logs a message at INFO level.
     *
     * @param message a literal message to log
     */
    public void info(String message)
    {
        logger.info(message);
    }

    /**
     * Logs a message at INFO level.
     *

     * Usage example:
     *

     *    logger.info("value is %s (%d ms)", value, time);
     *

     * If the format string is invalid or the arguments are insufficient, an error will be logged and execution
     * will continue.
     *
     * @param format a format string compatible with String.format()
     * @param args arguments for the format string
     */
    public void info(String format, Object... args)
    {
        if (logger.isLoggable(INFO)) {
            String message;
            try {
                message = format(format, args);
            }
            catch (IllegalFormatException e) {
                logger.log(SEVERE, illegalFormatMessageFor("INFO", format, args), e);
                message = rawMessageFor(format, args);
            }
            logger.info(message);
        }
    }

    /**
     * Logs a message at WARN level.
     *
     * @param exception an exception associated with the warning being logged
     * @param message a literal message to log
     */
    public void warn(Throwable exception, String message)
    {
        logger.log(WARNING, message, exception);
    }

    /**
     * Logs a message at WARN level.
     *
     * @param message a literal message to log
     */
    public void warn(String message)
    {
        logger.warning(message);
    }

    /**
     * Logs a message at WARN level.
     *

     * Usage example:
     *

     *    logger.warn(e, "something bad happened when connecting to %s:%d", host, port);
     *

     * If the format string is invalid or the arguments are insufficient, an error will be logged and execution
     * will continue.
     *
     * @param exception an exception associated with the warning being logged
     * @param format a format string compatible with String.format()
     * @param args arguments for the format string
     */
    public void warn(Throwable exception, String format, Object... args)
    {
        if (logger.isLoggable(WARNING)) {
            String message;
            try {
                message = format(format, args);
            }
            catch (IllegalFormatException e) {
                logger.log(SEVERE, illegalFormatMessageFor("WARN", format, args), e);
                message = rawMessageFor(format, args);
            }
            logger.log(WARNING, message, exception);
        }
    }

    /**
     * Logs a message at WARN level.
     *

     * Usage example:
     *

     *    logger.warn("something bad happened when connecting to %s:%d", host, port);
     *

     * If the format string is invalid or the arguments are insufficient, an error will be logged and execution
     * will continue.
     *
     * @param format a format string compatible with String.format()
     * @param args arguments for the format string
     */
    public void warn(String format, Object... args)
    {
        warn(null, format, args);
    }

    /**
     * Logs a message at ERROR level.
     *
     * @param exception an exception associated with the error being logged
     * @param message a literal message to log
     */
    public void error(Throwable exception, String message)
    {
        logger.log(SEVERE, message, exception);
    }

    /**
     * Logs a message at ERROR level.
     *
     * @param message a literal message to log
     */
    public void error(String message)
    {
        logger.severe(message);
    }

    /**
     * Logs a message at ERROR level.
     *

     * Usage example:
     *

     *    logger.error(e, "something really bad happened when connecting to %s:%d", host, port);
     *

     * If the format string is invalid or the arguments are insufficient, an error will be logged and execution
     * will continue.
     *
     * @param exception an exception associated with the error being logged
     * @param format a format string compatible with String.format()
     * @param args arguments for the format string
     */
    public void error(Throwable exception, String format, Object... args)
    {
        if (logger.isLoggable(SEVERE)) {
            String message;
            try {
                message = format(format, args);
            }
            catch (IllegalFormatException e) {
                logger.log(SEVERE, illegalFormatMessageFor("ERROR", format, args), e);
                message = rawMessageFor(format, args);
            }
            logger.log(SEVERE, message, exception);
        }
    }

    /**
     * Logs a message at ERROR level. The value of {@code exception.getMessage()} will be used as the log message.
     *

     * Usage example:
     *

     *    logger.error(e);
     *

     *
     * @param exception an exception associated with the error being logged
     */
    public void error(Throwable exception)
    {
        if (logger.isLoggable(SEVERE)) {
            logger.log(SEVERE, exception.getMessage(), exception);
        }
    }

    /**
     * Logs a message at ERROR level.
     *

     * Usage example:
     *

     *    logger.error("something really bad happened when connecting to %s:%d", host, port);
     *

     * If the format string is invalid or the arguments are insufficient, an error will be logged and execution
     * will continue.
     *
     * @param format a format string compatible with String.format()
     * @param args arguments for the format string
     */
    public void error(String format, Object... args)
    {
        error(null, format, args);
    }

    public boolean isDebugEnabled()
    {
        return logger.isLoggable(FINE);
    }

    public boolean isInfoEnabled()
    {
        return logger.isLoggable(INFO);
    }

    private String illegalFormatMessageFor(String level, String message, Object... args)
    {
        return format("Invalid format string while trying to log: %s '%s' %s", level, message, asList(args));
    }

    private String rawMessageFor(String format, Object... args)
    {
        return format("'%s' %s", format, asList(args));
    }
}


package io.airlift.http.client;

import com.google.common.annotations.Beta;
import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.net.HostAndPort;
import com.google.common.primitives.Bytes;

import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.util.Iterator;
import java.util.Map;

import static com.google.common.base.CharMatcher.ascii;
import static java.lang.Character.forDigit;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;

/**
 * An RFC-3986-compatible HTTP URI builder
 */
@Beta
public class HttpUriBuilder
{
    private String scheme;
    private String host;
    private int port = -1;
    private String path = ""; // decoded path
    private final ListMultimap<String, String> params = LinkedListMultimap.create(); // decoded query params

    private static final byte[] PCHAR = {
            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            '-', '.', '_', '~', '!', '$', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@',
    };

    private static final byte[] ALLOWED_PATH_CHARS = Bytes.concat(PCHAR, new byte[] {'/', '&'});
    private static final byte[] ALLOWED_QUERY_CHARS = Bytes.concat(PCHAR, new byte[] {'/', '?'});

    private HttpUriBuilder()
    {
    }

    private HttpUriBuilder(URI previous)
    {
        scheme = previous.getScheme();
        host = previous.getHost();
        port = previous.getPort();
        path = percentDecode(previous.getRawPath());
        params.putAll(parseParams(previous.getRawQuery()));
    }

    public static HttpUriBuilder uriBuilder()
    {
        return new HttpUriBuilder();
    }

    public static HttpUriBuilder uriBuilderFrom(URI uri)
    {
        requireNonNull(uri, "uri is null");

        return new HttpUriBuilder(uri);
    }

    public HttpUriBuilder scheme(String scheme)
    {
        requireNonNull(scheme, "scheme is null");

        this.scheme = scheme;
        return this;
    }

    public HttpUriBuilder host(String host)
    {
        requireNonNull(host, "host is null");
        Preconditions.checkArgument(!host.startsWith("["), "host starts with a bracket");
        Preconditions.checkArgument(!host.endsWith("]"), "host ends with a bracket");
        if (host.contains(":")) {
            host = "[" + host + "]";
        }
        this.host = host;
        return this;
    }

    public HttpUriBuilder port(int port)
    {
        Preconditions.checkArgument(port >= 1 && port <= 65535, "port must be in the range 1-65535");
        this.port = port;
        return this;
    }

    public HttpUriBuilder defaultPort()
    {
        this.port = -1;
        return this;
    }

    public HttpUriBuilder hostAndPort(HostAndPort hostAndPort)
    {
        requireNonNull(hostAndPort, "hostAndPort is null");
        this.host = bracketedHostString(hostAndPort);
        this.port = hostAndPort.hasPort() ? hostAndPort.getPort() : -1;
        return this;
    }

    /**
     * Replace the current path with the given unencoded path
     */
    public HttpUriBuilder replacePath(String path)
    {
        requireNonNull(path, "path is null");

        if (!path.isEmpty() && !path.startsWith("/")) {
            path = "/" + path;
        }

        this.path = path;
        return this;
    }

    /**
     * Append an unencoded path.
     * <p>
     * All reserved characters except '/' will be percent-encoded. '/' are considered as path separators and
     * appended verbatim.
     */
    public HttpUriBuilder appendPath(String path)
    {
        requireNonNull(path, "path is null");

        StringBuilder builder = new StringBuilder(this.path);
        if (!this.path.endsWith("/")) {
            builder.append('/');
        }

        if (path.startsWith("/")) {
            path = path.substring(1);
        }

        builder.append(path);

        this.path = builder.toString();

        return this;
    }

    public HttpUriBuilder replaceParameter(String name, String... values)
    {
        return replaceParameter(name, asList(values));
    }

    public HttpUriBuilder replaceParameter(String name, Iterable<String> values)
    {
        requireNonNull(name, "name is null");

        params.removeAll(name);
        addParameter(name, values);

        return this;
    }

    public HttpUriBuilder addParameter(String name, String... values)
    {
        return addParameter(name, asList(values));
    }

    public HttpUriBuilder addParameter(String name, Iterable<String> values)
    {
        requireNonNull(name, "name is null");

        if (Iterables.isEmpty(values)) {
            params.put(name, null);
        }

        for (String value : values) {
            params.put(name, value);
        }

        return this;
    }

    // return an RFC-3986-compatible URI
    public String toString()
    {
        StringBuilder builder = new StringBuilder();
        builder.append(scheme);
        builder.append("://");
        if (host != null) {
            builder.append(host);
        }
        if (port != -1) {
            builder.append(':');
            builder.append(port);
        }

        String path = this.path;
        if (path.isEmpty() && !params.isEmpty()) {
            path = "/";
        }

        builder.append(encode(path, ALLOWED_PATH_CHARS));

        if (!params.isEmpty()) {
            builder.append('?');

            for (Iterator<Map.Entry<String, String>> iterator = params.entries().iterator(); iterator.hasNext(); ) {
                Map.Entry<String, String> entry = iterator.next();

                builder.append(encode(entry.getKey(), ALLOWED_QUERY_CHARS));
                if (entry.getValue() != null) {
                    builder.append('=');
                    builder.append(encode(entry.getValue(), ALLOWED_QUERY_CHARS));
                }

                if (iterator.hasNext()) {
                    builder.append('&');
                }
            }
        }

        return builder.toString();
    }

    public URI build()
    {
        Preconditions.checkState(scheme != null, "scheme has not been set");
        return URI.create(toString());
    }

    private ListMultimap<String, String> parseParams(String query)
    {
        LinkedListMultimap<String, String> result = LinkedListMultimap.create();

        if (query != null) {
            Iterable<String> pairs = Splitter.on("&")
                    .omitEmptyStrings()
                    .split(query);

            for (String pair : pairs) {
                String[] parts = pair.split("=", 2);
                result.put(percentDecode(parts[0]), percentDecode(parts[1]));
            }
        }

        return result;
    }

    private String encode(String input, byte... allowed)
    {
        StringBuilder builder = new StringBuilder();

        ByteBuffer buffer = UTF_8.encode(input);
        while (buffer.remaining() > 0) {
            byte b = buffer.get();

            if (Bytes.contains(allowed, b)) {
                builder.append((char) b); // b is ASCII
            }
            else {
                builder.append('%');
                builder.append(Ascii.toUpperCase(forDigit((b >>> 4) & 0xF, 16)));
                builder.append(Ascii.toUpperCase(forDigit(b & 0xF, 16)));
            }
        }

        return builder.toString();
    }

    /**
     * input must be an ASCII string representing a percent-encoded UTF-8 byte sequence
     */
    private static String percentDecode(String encoded)
    {
        Preconditions.checkArgument(ascii().matchesAllOf(encoded), "string must be ASCII");

        ByteArrayOutputStream out = new ByteArrayOutputStream(encoded.length());
        for (int i = 0; i < encoded.length(); i++) {
            char c = encoded.charAt(i);

            if (c == '%') {
                Preconditions.checkArgument(i + 2 < encoded.length(), "percent encoded value is truncated");

                int high = Character.digit(encoded.charAt(i + 1), 16);
                int low = Character.digit(encoded.charAt(i + 2), 16);

                Preconditions.checkArgument(high != -1 && low != -1, "percent encoded value is not a valid hex string: ", encoded.substring(i, i + 2));

                int value = (high << 4) | (low);
                out.write(value);
                i += 2;
            }
            else {
                out.write((int) c);
            }
        }

        try {
            return UTF_8.newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .decode(ByteBuffer.wrap(out.toByteArray()))
                    .toString();
        }
        catch (CharacterCodingException e) {
            throw new IllegalArgumentException("input does not represent a proper UTF8-encoded string");
        }
    }

    private static String bracketedHostString(HostAndPort hostAndPort)
    {
        // HostAndPort cannot return just the bracketed host
        String host = hostAndPort.getHost();
        if (hostAndPort.toString().startsWith("[")) {
            return "[" + host + "]";
        }
        return host;
    }
}


package io.airlift.stats;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Ticker;
import org.weakref.jmx.Managed;

import java.util.concurrent.TimeUnit;

import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

/*
 * A counter that decays exponentially. Values are weighted according to the formula
 *     w(t, α) = e^(-α * t), where α is the decay factor and t is the age in seconds
 *
 * The implementation is based on the ideas from
 * http://dimacs.rutgers.edu/~graham/pubs/papers/fwddecay.pdf
 * to not have to rely on a timer that decays the value periodically
 */
public class DecayCounter
{
    // needs to be such that Math.exp(alpha * seconds) does not grow too big
    static final long RESCALE_THRESHOLD_SECONDS = 50;

    private final double alpha;
    private final Ticker ticker;

    private long landmarkInSeconds;
    private double count;

    public DecayCounter(double alpha)
    {
        this(alpha, Ticker.systemTicker());
    }

    public DecayCounter(double alpha, Ticker ticker)
    {
        this.alpha = alpha;
        this.ticker = ticker;
        landmarkInSeconds = getTickInSeconds();
    }

    public synchronized void add(long value)
    {
        long nowInSeconds = getTickInSeconds();

        if (nowInSeconds - landmarkInSeconds >= RESCALE_THRESHOLD_SECONDS) {
            rescaleToNewLandmark(nowInSeconds);
        }
        count += value * weight(nowInSeconds, landmarkInSeconds);
    }

    public synchronized void merge(DecayCounter decayCounter)
    {
        requireNonNull(decayCounter, "decayCounter is null");
        checkArgument(decayCounter.alpha == alpha, "Expected decayCounter to have alpha %s, but was %s", alpha, decayCounter.alpha);

        synchronized (decayCounter) {
            // if the landmark this counter is behind the other counter
            if (landmarkInSeconds < decayCounter.landmarkInSeconds) {
                // rescale this counter to the other counter, and add
                rescaleToNewLandmark(decayCounter.landmarkInSeconds);
                count += decayCounter.count;
            }
            else {
                // rescale the other counter and add
                double otherRescaledCount = decayCounter.count / weight(landmarkInSeconds, decayCounter.landmarkInSeconds);
                count += otherRescaledCount;
            }
        }
    }

    private void rescaleToNewLandmark(long newLandMarkInSeconds)
    {
        // rescale the count based on a new landmark to avoid numerical overflow issues
        count = count / weight(newLandMarkInSeconds, landmarkInSeconds);
        landmarkInSeconds = newLandMarkInSeconds;
    }

    @Managed
    public synchronized void reset()
    {
        landmarkInSeconds = getTickInSeconds();
        count = 0;
    }

    /**
     * This is a hack to work around limitations in Jmxutils.
     */
    @Deprecated
    public synchronized void resetTo(DecayCounter counter)
    {
        synchronized (counter) {
            landmarkInSeconds = counter.landmarkInSeconds;
            count = counter.count;
        }
    }

    @Managed
    public synchronized double getCount()
    {
        long nowInSeconds = getTickInSeconds();
        return count / weight(nowInSeconds, landmarkInSeconds);
    }

    @Managed
    public synchronized double getRate()
    {
        // The total time covered by this counter is equivalent to the integral of the weight function from 0 to Infinity,
        // which equals 1/alpha. The count per unit time is, therefore, count / (1/alpha)
        return getCount() * alpha;
    }

    private double weight(long timestampInSeconds, long landmarkInSeconds)
    {
        return Math.exp(alpha * (timestampInSeconds - landmarkInSeconds));
    }

    private long getTickInSeconds()
    {
        return TimeUnit.NANOSECONDS.toSeconds(ticker.read());
    }

    public DecayCounterSnapshot snapshot()
    {
        return new DecayCounterSnapshot(getCount(), getRate());
    }

    @Override
    public String toString()
    {
        return toStringHelper(this)
                .add("count", getCount())
                .add("rate", getRate())
                .toString();
    }

    public double getAlpha()
    {
        return alpha;
    }

    public static class DecayCounterSnapshot
    {
        private final double count;
        private final double rate;

        @JsonCreator
        public DecayCounterSnapshot(@JsonProperty("count") double count, @JsonProperty("rate") double rate)
        {
            this.count = count;
            this.rate = rate;
        }

        @JsonProperty
        public double getCount()
        {
            return count;
        }

        @JsonProperty
        public double getRate()
        {
            return rate;
        }

        @Override
        public String toString()
        {
            return toStringHelper(this)
                    .add("count", count)
                    .add("rate", rate)
                    .toString();
        }
    }
}


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

import org.eclipse.jetty.server.HttpChannel.Listener;
import org.eclipse.jetty.server.Request;

import javax.annotation.Nullable;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.DoubleSummaryStatistics;
import java.util.List;

import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

public class HttpServerChannelListener
        implements Listener
{
    private static final String REQUEST_BEGIN_ATTRIBUTE = HttpServerChannelListener.class.getName() + ".begin";
    private static final String REQUEST_BEGIN_TO_DISPATCH_ATTRIBUTE = HttpServerChannelListener.class.getName() + ".begin_to_dispatch";
    private static final String REQUEST_BEGIN_TO_END_ATTRIBUTE = HttpServerChannelListener.class.getName() + ".begin_to_end";
    private static final String RESPONSE_CONTENT_TIMESTAMPS_ATTRIBUTE = HttpServerChannelListener.class.getName() + ".response_content_timestamps";

    private final DelimitedRequestLog logger;

    public HttpServerChannelListener(DelimitedRequestLog logger)
    {
        this.logger = requireNonNull(logger, "logger is null");
    }

    @Override
    public void onRequestBegin(Request request)
    {
        request.setAttribute(REQUEST_BEGIN_ATTRIBUTE, System.nanoTime());
    }

    @SuppressWarnings("unchecked")
    @Override
    public void onBeforeDispatch(Request request)
    {
        long requestBeginTime = (Long) request.getAttribute(REQUEST_BEGIN_ATTRIBUTE);
        request.setAttribute(REQUEST_BEGIN_TO_DISPATCH_ATTRIBUTE, System.nanoTime() - requestBeginTime);
    }

    @SuppressWarnings("unchecked")
    @Override
    public void onRequestEnd(Request request)
    {
        long requestBeginTime = (Long) request.getAttribute(REQUEST_BEGIN_ATTRIBUTE);
        request.setAttribute(REQUEST_BEGIN_TO_END_ATTRIBUTE, System.nanoTime() - requestBeginTime);
    }

    @Override
    public void onResponseBegin(Request request)
    {
        if (request.getAttribute(REQUEST_BEGIN_TO_END_ATTRIBUTE) == null) {
            onRequestEnd(request);
        }
        request.setAttribute(RESPONSE_CONTENT_TIMESTAMPS_ATTRIBUTE, new ArrayList<Long>());
    }

    @SuppressWarnings("unchecked")
    @Override
    public void onResponseContent(Request request, ByteBuffer content)
    {
        List<Long> contentTimestamps = (List<Long>) request.getAttribute(RESPONSE_CONTENT_TIMESTAMPS_ATTRIBUTE);
        contentTimestamps.add(System.nanoTime());
    }

    @SuppressWarnings("unchecked")
    @Override
    public void onComplete(Request request)
    {
        List<Long> contentTimestamps = (List<Long>) request.getAttribute(RESPONSE_CONTENT_TIMESTAMPS_ATTRIBUTE);
        long firstToLastContentTimeInMillis = -1;
        if (contentTimestamps.size() > 0) {
            firstToLastContentTimeInMillis = NANOSECONDS.toMillis(contentTimestamps.get(contentTimestamps.size() - 1) - contentTimestamps.get(0));
        }
        long beginToDispatchMillis = NANOSECONDS.toMillis((Long) request.getAttribute(REQUEST_BEGIN_TO_DISPATCH_ATTRIBUTE));
        long beginToEndMillis = NANOSECONDS.toMillis((Long) request.getAttribute(REQUEST_BEGIN_TO_END_ATTRIBUTE));
        logger.log(request,
                request.getResponse(),
                beginToDispatchMillis,
                beginToEndMillis,
                firstToLastContentTimeInMillis,
                processContentTimestamps(contentTimestamps));
    }

    /**
     * Calculate the summary statistics for the interarrival time of the onResponseContent callbacks.
     */
    @Nullable
    private static DoubleSummaryStats processContentTimestamps(List<Long> contentTimestamps)
    {
        requireNonNull(contentTimestamps, "contentTimestamps is null");

        // no content (HTTP 204) or there was a single response chunk (so no interarrival time)
        if (contentTimestamps.size() == 0 || contentTimestamps.size() == 1) {
            return null;
        }

        DoubleSummaryStatistics statistics = new DoubleSummaryStatistics();
        long previousTimestamp = contentTimestamps.get(0);
        for (int i = 1; i < contentTimestamps.size(); i++) {
            long timestamp = contentTimestamps.get(i);
            statistics.accept(NANOSECONDS.toMillis(timestamp - previousTimestamp));
            previousTimestamp = timestamp;
        }
        return new DoubleSummaryStats(statistics);
    }
}


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

import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.inject.Binding;
import com.google.inject.ConfigurationException;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.spi.DefaultElementVisitor;
import com.google.inject.spi.Element;
import com.google.inject.spi.Elements;
import com.google.inject.spi.InstanceBinding;
import com.google.inject.spi.Message;
import com.google.inject.spi.ProviderInstanceBinding;
import io.airlift.configuration.ConfigurationMetadata.AttributeMetadata;
import org.apache.bval.jsr.ApacheValidationProvider;

import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.inject.Provider;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Consumer;
import java.util.function.Function;

import static com.google.common.base.CaseFormat.LOWER_CAMEL;
import static com.google.common.base.CaseFormat.UPPER_CAMEL;
import static com.google.common.collect.Sets.newConcurrentHashSet;
import static io.airlift.configuration.ConfigurationMetadata.getConfigurationMetadata;
import static io.airlift.configuration.Problems.exceptionFor;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

public class ConfigurationFactory
{
    @GuardedBy("VALIDATOR")
    private static final Validator VALIDATOR;

    static {
        // this prevents bval from using the thread context classloader
        ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(null);
            VALIDATOR = Validation.byProvider(ApacheValidationProvider.class).configure().buildValidatorFactory().getValidator();
        }
        finally {
            Thread.currentThread().setContextClassLoader(currentClassLoader);
        }
    }

    private final Map<String, String> properties;
    private final WarningsMonitor warningsMonitor;
    private final Problems.Monitor monitor;
    private final ConcurrentMap<ConfigurationProvider<?>, Object> instanceCache = new ConcurrentHashMap<>();
    private final Set<String> usedProperties = newConcurrentHashSet();
    private final Set<ConfigurationProvider<?>> registeredProviders = newConcurrentHashSet();
    @GuardedBy("this")
    private final List<Consumer<ConfigurationProvider<?>>> configurationBindingListeners = new ArrayList<>();
    private final ListMultimap<Key<?>, ConfigDefaultsHolder<?>> registeredDefaultConfigs = Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
    private final LoadingCache<Class<?>, ConfigurationMetadata<?>> metadataCache = CacheBuilder.newBuilder()
            .build(new CacheLoader<Class<?>, ConfigurationMetadata<?>>()
            {
                @Override
                public ConfigurationMetadata<?> load(Class<?> configClass)
                {
                    return getConfigurationMetadata(configClass, monitor);
                }
            });

    public ConfigurationFactory(Map<String, String> properties)
    {
        this(properties, null, Problems.NULL_MONITOR);
    }

    public ConfigurationFactory(Map<String, String> properties, WarningsMonitor warningsMonitor)
    {
        this(properties, warningsMonitor, Problems.NULL_MONITOR);
    }

    @VisibleForTesting
    ConfigurationFactory(Map<String, String> properties, WarningsMonitor warningsMonitor, Problems.Monitor monitor)
    {
        this.properties = ImmutableMap.copyOf(properties);
        this.warningsMonitor = warningsMonitor;
        this.monitor = monitor;
    }

    public Map<String, String> getProperties()
    {
        return properties;
    }

    /**
     * Marks the specified property as consumed.
     */
    @Beta
    public void consumeProperty(String property)
    {
        requireNonNull(property, "property is null");
        usedProperties.add(property);
    }

    public Set<String> getUsedProperties()
    {
        return ImmutableSortedSet.copyOf(usedProperties);
    }

    /**
     * Registers all configuration classes in the module so they can be part of configuration inspection.
     */
    @Beta
    public void registerConfigurationClasses(Module module)
    {
        registerConfigurationClasses(ImmutableList.of(module));
    }

    public void registerConfigurationClasses(Collection<? extends Module> modules)
    {
        // some modules need access to configuration factory so they can lazy register additional config classes
        // initialize configuration factory
        modules.stream()
                .filter(ConfigurationAwareModule.class::isInstance)
                .map(ConfigurationAwareModule.class::cast)
                .forEach(module -> module.setConfigurationFactory(this));

        for (Element element : Elements.getElements(modules)) {
            element.acceptVisitor(new DefaultElementVisitor<Void>()
            {
                @Override
                public <T> Void visit(Binding<T> binding)
                {
                    if (binding instanceof InstanceBinding) {
                        InstanceBinding<T> instanceBinding = (InstanceBinding<T>) binding;

                        // configuration listener
                        if (instanceBinding.getInstance() instanceof ConfigurationBindingListenerHolder) {
                            addConfigurationBindingListener(((ConfigurationBindingListenerHolder) instanceBinding.getInstance()).getConfigurationBindingListener());
                        }

                        // config defaults
                        if (instanceBinding.getInstance() instanceof ConfigDefaultsHolder) {
                            registerConfigDefaults((ConfigDefaultsHolder<?>) instanceBinding.getInstance());
                        }
                    }

                    // configuration provider
                    if (binding instanceof ProviderInstanceBinding) {
                        ProviderInstanceBinding<?> providerInstanceBinding = (ProviderInstanceBinding<?>) binding;
                        Provider<?> provider = providerInstanceBinding.getUserSuppliedProvider();
                        if (provider instanceof ConfigurationProvider) {
                            registerConfigurationProvider((ConfigurationProvider<?>) provider, Optional.of(binding.getSource()));
                        }
                    }
                    return null;
                }
            });
        }
    }

    void registerConfigurationProvider(ConfigurationProvider<?> configurationProvider, Optional<Object> bindingSource)
    {
        configurationProvider.setConfigurationFactory(this);
        configurationProvider.setBindingSource(bindingSource);

        ImmutableList<Consumer<ConfigurationProvider<?>>> listeners = ImmutableList.of();
        synchronized (this) {
            if (registeredProviders.add(configurationProvider)) {
                listeners = ImmutableList.copyOf(configurationBindingListeners);
            }
        }
        listeners.forEach(listener -> listener.accept(configurationProvider));
    }

    public void addConfigurationBindingListener(ConfigurationBindingListener listener)
    {
        ConfigurationProviderConsumer consumer = new ConfigurationProviderConsumer(listener);

        ImmutableSet<ConfigurationProvider<?>> currentProviders;
        synchronized (this) {
            configurationBindingListeners.add(consumer);
            currentProviders = ImmutableSet.copyOf(registeredProviders);
        }
        currentProviders.forEach(consumer);
    }

    public List<Message> validateRegisteredConfigurationProvider()
    {
        List<Message> messages = new ArrayList<>();
        for (ConfigurationProvider<?> configurationProvider : ImmutableList.copyOf(registeredProviders)) {
            try {
                // call the getter which will cause object creation
                configurationProvider.get();
            }
            catch (ConfigurationException e) {
                // if we got errors, add them to the errors list
                ImmutableList<Object> sources = configurationProvider.getBindingSource().map(ImmutableList::of).orElse(ImmutableList.of());
                for (Message message : e.getErrorMessages()) {
                    messages.add(new Message(sources, message.getMessage(), message.getCause()));
                }
            }
        }
        return messages;
    }

    Iterable<ConfigurationProvider<?>> getConfigurationProviders()
    {
        return ImmutableList.copyOf(registeredProviders);
    }

    <T> void registerConfigDefaults(ConfigDefaultsHolder<T> holder)
    {
        registeredDefaultConfigs.put(holder.getConfigKey(), holder);
    }

    private <T> ConfigDefaults<T> getConfigDefaults(Key<T> key)
    {
        ImmutableList.Builder<ConfigDefaults<T>> defaults = ImmutableList.builder();

        Key<?> globalDefaults = Key.get(key.getTypeLiteral(), GlobalDefaults.class);
        registeredDefaultConfigs.get(globalDefaults).stream()
                .map(ConfigurationFactory.<T>castHolder())
                .sorted()
                .map(ConfigDefaultsHolder::getConfigDefaults)
                .forEach(defaults::add);

        registeredDefaultConfigs.get(key).stream()
                .map(ConfigurationFactory.<T>castHolder())
                .sorted()
                .map(ConfigDefaultsHolder::getConfigDefaults)
                .forEach(defaults::add);

        return ConfigDefaults.configDefaults(defaults.build());
    }

    @SuppressWarnings("unchecked")
    private static <T> Function<ConfigDefaultsHolder<?>, ConfigDefaultsHolder<T>> castHolder()
    {
        return holder -> (ConfigDefaultsHolder<T>) holder;
    }

    <T> T getDefaultConfig(Key<T> key)
    {
        ConfigurationMetadata<T> configurationMetadata = getMetadata(key);
        configurationMetadata.getProblems().throwIfHasErrors();

        T instance = newInstance(configurationMetadata);

        ConfigDefaults<T> configDefaults = getConfigDefaults(key);
        configDefaults.setDefaults(instance);

        return instance;
    }

    public <T> T build(Class<T> configClass)
    {
        return build(configClass, null);
    }

    public <T> T build(Class<T> configClass, @Nullable String prefix)
    {
        return build(configClass, Optional.ofNullable(prefix), ConfigDefaults.noDefaults()).getInstance();
    }

    /**
     * This is used by the configuration provider
     */
    <T> T build(ConfigurationProvider<T> configurationProvider)
    {
        requireNonNull(configurationProvider, "configurationProvider");
        registerConfigurationProvider(configurationProvider, Optional.empty());

        // check for a prebuilt instance
        T instance = getCachedInstance(configurationProvider);
        if (instance != null) {
            return instance;
        }

        ConfigurationBinding<T> configurationBinding = configurationProvider.getConfigurationBinding();
        ConfigurationHolder<T> holder = build(configurationBinding.getConfigClass(), configurationBinding.getPrefix(), getConfigDefaults(configurationBinding.getKey()));
        instance = holder.getInstance();

        // inform caller about warnings
        if (warningsMonitor != null) {
            for (Message message : holder.getProblems().getWarnings()) {
                warningsMonitor.onWarning(message.toString());
            }
        }

        // add to instance cache
        T existingValue = putCachedInstance(configurationProvider, instance);
        // if key was already associated with a value, there was a
        // creation race and we lost. Just use the winners' instance;
        if (existingValue != null) {
            return existingValue;
        }
        return instance;
    }

    @SuppressWarnings("unchecked")
    private <T> T getCachedInstance(ConfigurationProvider<T> configurationProvider)
    {
        return (T) instanceCache.get(configurationProvider);
    }

    @SuppressWarnings("unchecked")
    private <T> T putCachedInstance(ConfigurationProvider<T> configurationProvider, T instance)
    {
        return (T) instanceCache.putIfAbsent(configurationProvider, instance);
    }

    private <T> ConfigurationHolder<T> build(Class<T> configClass, Optional<String> configPrefix, ConfigDefaults<T> configDefaults)
    {
        if (configClass == null) {
            throw new NullPointerException("configClass is null");
        }

        String prefix = configPrefix
                .map(value -> value + ".")
                .orElse("");

        ConfigurationMetadata<T> configurationMetadata = getMetadata(configClass);
        configurationMetadata.getProblems().throwIfHasErrors();

        T instance = newInstance(configurationMetadata);

        configDefaults.setDefaults(instance);

        Problems problems = new Problems(monitor);

        for (AttributeMetadata attribute : configurationMetadata.getAttributes().values()) {
            try {
                setConfigProperty(instance, attribute, prefix, problems);
            }
            catch (InvalidConfigurationException e) {
                problems.addError(e.getCause(), e.getMessage());
            }
        }

        // Check that none of the defunct properties are still in use
        if (configClass.isAnnotationPresent(DefunctConfig.class)) {
            for (String value : configClass.getAnnotation(DefunctConfig.class).value()) {
                String name = prefix + value;
                if (!value.isEmpty() && properties.get(name) != null) {
                    problems.addError("Defunct property '%s' (class [%s]) cannot be configured.", name, configClass.toString());
                }
            }
        }

        for (ConstraintViolation<?> violation : validate(instance)) {
            String propertyFieldName = violation.getPropertyPath().toString();
            // upper case first character to match config attribute name
            String attributeName = LOWER_CAMEL.to(UPPER_CAMEL, propertyFieldName);
            AttributeMetadata attribute = configurationMetadata.getAttributes().get(attributeName);
            if (attribute != null && attribute.getInjectionPoint() != null) {
                String propertyName = attribute.getInjectionPoint().getProperty();
                if (!prefix.isEmpty()) {
                    propertyName = prefix + propertyName;
                }
                problems.addError("Invalid configuration property %s: %s (for class %s.%s)",
                        propertyName, violation.getMessage(), configClass.getName(), violation.getPropertyPath());
            }
            else {
                problems.addError("Invalid configuration property with prefix '%s': %s (for class %s.%s)",
                        prefix, violation.getMessage(), configClass.getName(), violation.getPropertyPath());
            }
        }

        problems.throwIfHasErrors();

        return new ConfigurationHolder<>(instance, problems);
    }

    private static <T> Set<ConstraintViolation<T>> validate(T instance)
    {
        synchronized (VALIDATOR) {
            return VALIDATOR.validate(instance);
        }
    }

    @SuppressWarnings("unchecked")
    private <T> ConfigurationMetadata<T> getMetadata(Key<T> key)
    {
        return getMetadata((Class<T>) key.getTypeLiteral().getRawType());
    }

    @SuppressWarnings("unchecked")
    private <T> ConfigurationMetadata<T> getMetadata(Class<T> configClass)
    {
        return (ConfigurationMetadata<T>) metadataCache.getUnchecked(configClass);
    }

    private static <T> T newInstance(ConfigurationMetadata<T> configurationMetadata)
    {
        try {
            return configurationMetadata.getConstructor().newInstance();
        }
        catch (Throwable e) {
            if (e instanceof InvocationTargetException && e.getCause() != null) {
                e = e.getCause();
            }
            throw exceptionFor(e, "Error creating instance of configuration class [%s]", configurationMetadata.getConfigClass().getName());
        }
    }

    private <T> void setConfigProperty(T instance, AttributeMetadata attribute, String prefix, Problems problems)
            throws InvalidConfigurationException
    {
        // Get property value
        ConfigurationMetadata.InjectionPointMetaData injectionPoint = findOperativeInjectionPoint(attribute, prefix, problems);

        // If we did not get an injection point, do not call the setter
        if (injectionPoint == null) {
            return;
        }

        if (injectionPoint.getSetter().isAnnotationPresent(Deprecated.class)) {
            problems.addWarning("Configuration property '%s' is deprecated and should not be used", prefix + injectionPoint.getProperty());
        }

        Object value = getInjectedValue(attribute, injectionPoint, prefix);

        try {
            injectionPoint.getSetter().invoke(instance, value);
        }
        catch (Throwable e) {
            if (e instanceof InvocationTargetException && e.getCause() != null) {
                e = e.getCause();
            }
            throw new InvalidConfigurationException(e, format("Error invoking configuration method [%s]", injectionPoint.getSetter().toGenericString()));
        }
    }

    private ConfigurationMetadata.InjectionPointMetaData findOperativeInjectionPoint(AttributeMetadata attribute, String prefix, Problems problems)
            throws ConfigurationException
    {
        ConfigurationMetadata.InjectionPointMetaData operativeInjectionPoint = attribute.getInjectionPoint();
        String operativeName = null;
        String operativeValue = null;
        if (operativeInjectionPoint != null) {
            operativeName = prefix + operativeInjectionPoint.getProperty();
            operativeValue = properties.get(operativeName);
        }
        String printableOperativeValue = operativeValue;
        if (attribute.isSecuritySensitive()) {
            printableOperativeValue = "[REDACTED]";
        }

        for (ConfigurationMetadata.InjectionPointMetaData injectionPoint : attribute.getLegacyInjectionPoints()) {
            String fullName = prefix + injectionPoint.getProperty();
            String value = properties.get(fullName);
            String printableValue = value;
            if (attribute.isSecuritySensitive()) {
                printableValue = "[REDACTED]";
            }
            if (value != null) {
                String replacement = "deprecated.";
                if (attribute.getInjectionPoint() != null) {
                    replacement = format("replaced. Use '%s' instead.", prefix + attribute.getInjectionPoint().getProperty());
                }
                problems.addWarning("Configuration property '%s' has been " + replacement, fullName);

                if (operativeValue == null) {
                    operativeInjectionPoint = injectionPoint;
                    operativeValue = value;
                    printableOperativeValue = printableValue;
                    operativeName = fullName;
                }
                else {
                    problems.addError("Configuration property '%s' (=%s) conflicts with property '%s' (=%s)", fullName, printableValue, operativeName, printableOperativeValue);
                }
            }
        }

        problems.throwIfHasErrors();
        if (operativeValue == null) {
            // No injection from configuration
            return null;
        }

        return operativeInjectionPoint;
    }

    private Object getInjectedValue(AttributeMetadata attribute, ConfigurationMetadata.InjectionPointMetaData injectionPoint, String prefix)
            throws InvalidConfigurationException
    {
        // Get the property value
        String name = prefix + injectionPoint.getProperty();
        String value = properties.get(name);
        String printableValue = value;
        if (attribute.isSecuritySensitive()) {
            printableValue = "[REDACTED]";
        }

        if (value == null) {
            return null;
        }

        // coerce the property value to the final type
        Class<?> propertyType = injectionPoint.getSetter().getParameterTypes()[0];

        Object finalValue = coerce(propertyType, value);
        if (finalValue == null) {
            throw new InvalidConfigurationException(format("Could not coerce value '%s' to %s (property '%s') in order to call [%s]",
                    printableValue,
                    propertyType.getName(),
                    name,
                    injectionPoint.getSetter().toGenericString()));
        }
        usedProperties.add(name);
        return finalValue;
    }

    private static Object coerce(Class<?> type, String value)
    {
        if (type.isPrimitive() && value == null) {
            return null;
        }

        try {
            if (String.class.isAssignableFrom(type)) {
                return value;
            }
            else if (Boolean.class.isAssignableFrom(type) || Boolean.TYPE.isAssignableFrom(type)) {
                return Boolean.valueOf(value);
            }
            else if (Byte.class.isAssignableFrom(type) || Byte.TYPE.isAssignableFrom(type)) {
                return Byte.valueOf(value);
            }
            else if (Short.class.isAssignableFrom(type) || Short.TYPE.isAssignableFrom(type)) {
                return Short.valueOf(value);
            }
            else if (Integer.class.isAssignableFrom(type) || Integer.TYPE.isAssignableFrom(type)) {
                return Integer.valueOf(value);
            }
            else if (Long.class.isAssignableFrom(type) || Long.TYPE.isAssignableFrom(type)) {
                return Long.valueOf(value);
            }
            else if (Float.class.isAssignableFrom(type) || Float.TYPE.isAssignableFrom(type)) {
                return Float.valueOf(value);
            }
            else if (Double.class.isAssignableFrom(type) || Double.TYPE.isAssignableFrom(type)) {
                return Double.valueOf(value);
            }
        }
        catch (Exception ignored) {
            // ignore the random exceptions from the built in types
            return null;
        }

        // Look for a static fromString(String) method
        try {
            Method fromString = type.getMethod("fromString", String.class);
            if (fromString.getReturnType().isAssignableFrom(type)) {
                return fromString.invoke(null, value);
            }
        }
        catch (Throwable ignored) {
        }

        // Look for a static valueOf(String) method
        try {
            Method valueOf = type.getMethod("valueOf", String.class);
            if (valueOf.getReturnType().isAssignableFrom(type)) {
                return valueOf.invoke(null, value);
            }
        }
        catch (Throwable ignored) {
        }

        // Look for a constructor taking a string
        try {
            Constructor<?> constructor = type.getConstructor(String.class);
            return constructor.newInstance(value);
        }
        catch (Throwable ignored) {
        }

        return null;
    }

    private static class ConfigurationHolder<T>
    {
        private final T instance;
        private final Problems problems;

        private ConfigurationHolder(T instance, Problems problems)
        {
            this.instance = instance;
            this.problems = problems;
        }

        public T getInstance()
        {
            return instance;
        }

        public Problems getProblems()
        {
            return problems;
        }
    }

    private class ConfigurationProviderConsumer
            implements Consumer<ConfigurationProvider<?>>
    {
        private final ConfigurationBindingListener listener;
        private final ConfigBinder configBinder;

        public ConfigurationProviderConsumer(ConfigurationBindingListener listener)
        {
            this.listener = listener;
            this.configBinder = ConfigBinder.configBinder(ConfigurationFactory.this, Optional.of(listener));
        }

        @Override
        public void accept(ConfigurationProvider<?> configurationProvider)
        {
            listener.configurationBound(configurationProvider.getConfigurationBinding(), configBinder);
        }
    }
}


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

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.Provider;
import java.security.Security;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static io.airlift.security.der.DerUtils.encodeLength;
import static java.util.Objects.requireNonNull;

public final class SignatureAlgorithmIdentifier
{
    private static final int OBJECT_IDENTIFIER_TAG = 0x06;
    private static final Map<String, SignatureAlgorithmIdentifier> ALGORITHMS;

    private static final String SIGNATURE_OID_PREFIX = "Alg.Alias.Signature.OID.";

    static {
        Map<String, SignatureAlgorithmIdentifier> algorithms = new LinkedHashMap<>();
        for (Provider provider : Security.getProviders()) {
            for (Entry<String, String> entry : Maps.fromProperties(provider).entrySet()) {
                if (entry.getKey().startsWith(SIGNATURE_OID_PREFIX)) {
                    String oid = entry.getKey().substring(SIGNATURE_OID_PREFIX.length());
                    SignatureAlgorithmIdentifier algorithmIdentifier = new SignatureAlgorithmIdentifier(entry.getValue(), oid);
                    algorithms.putIfAbsent(entry.getValue(), algorithmIdentifier);
                }
            }
        }
        ALGORITHMS = ImmutableMap.copyOf(algorithms);
    }

    public static Map<String, SignatureAlgorithmIdentifier> getAllSignatureAlgorithmIdentifiers()
    {
        return ALGORITHMS;
    }

    public static SignatureAlgorithmIdentifier findSignatureAlgorithmIdentifier(String algorithmName)
    {
        SignatureAlgorithmIdentifier identifier = ALGORITHMS.get(algorithmName);
        checkArgument(identifier != null, "Unknown signature algorithm '%s'", algorithmName);
        return identifier;
    }

    private final String name;
    private final String oid;
    private final byte[] encoded;

    public SignatureAlgorithmIdentifier(String name, String oid)
    {
        this.name = requireNonNull(name, "name is null");
        this.oid = requireNonNull(oid, "oid is null");

        List<Integer> parts = Splitter.on('.').splitToList(oid).stream()
                .map(Integer::parseInt)
                .collect(toImmutableList());
        checkArgument(parts.size() >= 2, "at least 2 parts are required");

        try {
            ByteArrayOutputStream body = new ByteArrayOutputStream();
            body.write(parts.get(0) * 40 + parts.get(1));
            for (Integer part : parts.subList(2, parts.size())) {
                writePart(body, part);
            }

            byte[] length = encodeLength(body.size());
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            out.write(OBJECT_IDENTIFIER_TAG);
            out.write(length);
            body.writeTo(out);
            encoded = out.toByteArray();
        }
        catch (IOException e) {
            // byte array output stream does not throw exceptions
            throw new RuntimeException(e);
        }
    }

    public String getName()
    {
        return name;
    }

    public String getOid()
    {
        return oid;
    }

    public byte[] getEncoded()
    {
        return encoded.clone();
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        SignatureAlgorithmIdentifier that = (SignatureAlgorithmIdentifier) o;
        return Objects.equals(oid, that.oid);
    }

    @Override
    public int hashCode()
    {
        return Objects.hash(oid);
    }

    @Override
    public String toString()
    {
        return toStringHelper(this)
                .add("name", name)
                .add("oid", oid)
                .toString();
    }

    /**
     * Encode an OID number part.  The encoding is a big endian varint.
     */
    private static void writePart(OutputStream out, final int number)
            throws IOException
    {
        if (number < 128) {
            out.write((byte) number);
            return;
        }

        int numberOfBits = Integer.SIZE - Integer.numberOfLeadingZeros(number);
        int numberOfParts = (numberOfBits + 6) / 7;
        for (int i = 0; i < numberOfParts - 1; i++) {
            int partToEncode = (numberOfParts - i);
            int shiftSize = (partToEncode - 1) * 7;
            int part = (number >>> shiftSize) & 0x7F | 0x80;
            out.write(part);
        }
        out.write(number & 0x7f);
    }
}


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

Copyright (c) 2010-2011 Coda Hale

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

Copied from https://github.com/codahale/metrics

*/

package io.airlift.stats;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static java.lang.Math.exp;
import static java.lang.Math.floor;
import static java.lang.Math.min;
import static java.lang.Math.random;

/**
 * An exponentially-decaying random sample of {@code long}s. Uses Cormode et
 * al's forward-decaying priority reservoir sampling method to produce a
 * statistically representative sample, exponentially biased towards newer
 * entries.
 *
 * @see
 * Cormode et al. Forward Decay: A Practical Time Decay Model for Streaming
 * Systems. ICDE '09: Proceedings of the 2009 IEEE International Conference on
 * Data Engineering (2009)
 */
final class ExponentiallyDecayingSample
{
    private static final long RESCALE_THRESHOLD = TimeUnit.HOURS.toNanos(1);
    private final ConcurrentSkipListMap<Double, Long> values;
    private final ReentrantReadWriteLock lock;
    private final double alpha;
    private final int reservoirSize;
    private final AtomicLong count = new AtomicLong(0);
    private volatile long startTime;
    private final AtomicLong nextScaleTime = new AtomicLong(0);

    /**
     * Creates a new {@link ExponentiallyDecayingSample}.
     *
     * @param reservoirSize the number of samples to keep in the sampling reservoir
     * @param alpha the exponential decay factor; the higher this is, the more
     * biased the sample will be towards newer values
     */
    public ExponentiallyDecayingSample(int reservoirSize, double alpha)
    {
        this.values = new ConcurrentSkipListMap<>();
        this.lock = new ReentrantReadWriteLock();
        this.alpha = alpha;
        this.reservoirSize = reservoirSize;
        clear();
    }

    public void clear()
    {
        values.clear();
        count.set(0);
        this.startTime = tick();
        nextScaleTime.set(System.nanoTime() + RESCALE_THRESHOLD);
    }

    public int size()
    {
        return (int) min(reservoirSize, count.get());
    }

    public void update(long value)
    {
        update(value, tick());
    }

    /**
     * Adds an old value with a fixed timestamp to the sample.
     *
     * @param value the value to be added
     * @param timestamp the epoch timestamp of {@code value} in seconds
     */
    public void update(long value, long timestamp)
    {
        lockForRegularUsage();
        try {
            final double priority = weight(timestamp - startTime) / random();
            final long newCount = count.incrementAndGet();
            if (newCount <= reservoirSize) {
                values.put(priority, value);
            }
            else {
                Double first = values.firstKey();
                if (first < priority) {
                    if (values.putIfAbsent(priority, value) == null) {
                        // ensure we always remove an item
                        while (values.remove(first) == null) {
                            first = values.firstKey();
                        }
                    }
                }
            }
        }
        finally {
            unlockForRegularUsage();
        }

        final long now = System.nanoTime();
        final long next = nextScaleTime.get();
        if (now >= next) {
            rescale(now, next);
        }
    }

    public List<Long> values()
    {
        lockForRegularUsage();
        try {
            return new ArrayList<>(values.values());
        }
        finally {
            unlockForRegularUsage();
        }
    }

    private static long tick()
    {
        return TimeUnit.NANOSECONDS.toSeconds(System.nanoTime());
    }

    private double weight(long t)
    {
        return exp(alpha * t);
    }

    /* "A common feature of the above techniques—indeed, the key technique that
     * allows us to track the decayed weights efficiently—is that they maintain
     * counts and other quantities based on g(ti − L), and only scale by g(t − L)
     * at query time. But while g(ti −L)/g(t−L) is guaranteed to lie between zero
     * and one, the intermediate values of g(ti − L) could become very large. For
     * polynomial functions, these values should not grow too large, and should be
     * effectively represented in practice by floating point values without loss of
     * precision. For exponential functions, these values could grow quite large as
     * new values of (ti − L) become large, and potentially exceed the capacity of
     * common floating point types. However, since the values stored by the
     * algorithms are linear combinations of g values (scaled sums), they can be
     * rescaled relative to a new landmark. That is, by the analysis of exponential
     * decay in Section III-A, the choice of L does not affect the final result. We
     * can therefore multiply each value based on L by a factor of exp(−α(L′ − L)),
     * and obtain the correct value as if we had instead computed relative to a new
     * landmark L′ (and then use this new L′ at query time). This can be done with
     * a linear pass over whatever data structure is being used."
     */
    private void rescale(long now, long next)
    {
        if (nextScaleTime.compareAndSet(next, now + RESCALE_THRESHOLD)) {
            lockForRescale();
            try {
                final long oldStartTime = startTime;
                this.startTime = tick();
                final ArrayList<Double> keys = new ArrayList<>(values.keySet());
                for (Double key : keys) {
                    final Long value = values.remove(key);
                    values.put(key * exp(-alpha * (startTime - oldStartTime)), value);
                }
            }
            finally {
                unlockForRescale();
            }
        }
    }

    private void unlockForRescale()
    {
        lock.writeLock().unlock();
    }

    private void lockForRescale()
    {
        lock.writeLock().lock();
    }

    private void lockForRegularUsage()
    {
        lock.readLock().lock();
    }

    private void unlockForRegularUsage()
    {
        lock.readLock().unlock();
    }

    public double[] percentiles(double... percentiles)
    {
        final double[] scores = new double[percentiles.length];
        Arrays.fill(scores, Double.NaN);

        final List<Long> values = this.values();
        if (!values.isEmpty()) {
            Collections.sort(values);

            for (int i = 0; i < percentiles.length; i++) {
                final double p = percentiles[i];
                final double pos = p * (values.size() + 1);
                if (pos < 1) {
                    scores[i] = values.get(0);
                }
                else if (pos >= values.size()) {
                    scores[i] = values.get(values.size() - 1);
                }
                else {
                    final double lower = values.get((int) pos - 1);
                    final double upper = values.get((int) pos);
                    scores[i] = lower + (pos - floor(pos)) * (upper - lower);
                }
            }
        }

        return scores;
    }
}


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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.charset.Charset;

final class HttpMBeanServerRpc
{
    public static final Charset UTF_8 = Charset.forName("UTF-8");

    private HttpMBeanServerRpc() {}

    public static byte[] createSuccessResponse(Object result)
            throws IOException
    {
        return serialize(result);
    }

    public static byte[] createExceptionResponse(Exception exception)
            throws IOException
    {
        return serialize(exception);
    }

    public static byte[] serialize(Object object)
            throws IOException
    {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(bytes);
        objectOutputStream.writeObject(object);
        objectOutputStream.flush();
        objectOutputStream.close();
        return bytes.toByteArray();
    }

    public static Object deserialize(InputStream inputStream)
            throws IOException
    {
        try {
            return new ObjectInputStream(inputStream).readObject();
        }
        catch (ClassNotFoundException e) {
            throw new IOException(e);
        }
    }

    //
    // This code was swiped from Guava
    //
    public static <X extends Throwable> void propagateIfInstanceOf(Throwable throwable, Class<X> declaredType)
            throws X
    {
        if (throwable != null && declaredType.isInstance(throwable)) {
            throw declaredType.cast(throwable);
        }
    }

    public static void propagateIfPossible(Throwable throwable)
    {
        propagateIfInstanceOf(throwable, Error.class);
        propagateIfInstanceOf(throwable, RuntimeException.class);
    }

    /**
     * Implements the "base64" binary encoding scheme as defined by
     * RFC 2045.
     *

     * Portions of code here are taken from Apache Pivot
     */
    private static final char[] lookup = new char[64];
    private static final byte[] reverseLookup = new byte[256];

    static {
        // Populate the lookup array

        for (int i = 0; i < 26; i++) {
            lookup[i] = (char) ('A' + i);
        }

        for (int i = 26, j = 0; i < 52; i++, j++) {
            lookup[i] = (char) ('a' + j);
        }

        for (int i = 52, j = 0; i < 62; i++, j++) {
            lookup[i] = (char) ('0' + j);
        }

        lookup[62] = '+';
        lookup[63] = '/';

        // Populate the reverse lookup array

        for (int i = 0; i < 256; i++) {
            reverseLookup[i] = -1;
        }

        for (int i = 'Z'; i >= 'A'; i--) {
            reverseLookup[i] = (byte) (i - 'A');
        }

        for (int i = 'z'; i >= 'a'; i--) {
            reverseLookup[i] = (byte) (i - 'a' + 26);
        }

        for (int i = '9'; i >= '0'; i--) {
            reverseLookup[i] = (byte) (i - '0' + 52);
        }

        reverseLookup['+'] = 62;
        reverseLookup['/'] = 63;
        reverseLookup['='] = 0;
    }

    public static String base64Encode(String data)
    {
        return base64Encode(data.getBytes(UTF_8));
    }

    /**
     * Encodes the specified data into a base64 string.
     *
     * @param bytes The unencoded raw data.
     */
    public static String base64Encode(byte[] bytes)
    {
        // always sequence of 4 characters for each 3 bytes; padded with '='s as necessary:
        StringBuilder buf = new StringBuilder(((bytes.length + 2) / 3) * 4);

        // first, handle complete chunks (fast loop)
        int i = 0;
        for (int end = bytes.length - 2; i < end; ) {
            int chunk = ((bytes[i++] & 0xFF) << 16)
                    | ((bytes[i++] & 0xFF) << 8)
                    | (bytes[i++] & 0xFF);
            buf.append(lookup[chunk >> 18]);
            buf.append(lookup[(chunk >> 12) & 0x3F]);
            buf.append(lookup[(chunk >> 6) & 0x3F]);
            buf.append(lookup[chunk & 0x3F]);
        }

        // then leftovers, if any
        int len = bytes.length;
        if (i < len) { // 1 or 2 extra bytes?
            int chunk = ((bytes[i++] & 0xFF) << 16);
            buf.append(lookup[chunk >> 18]);
            if (i < len) { // 2 bytes
                chunk |= ((bytes[i] & 0xFF) << 8);
                buf.append(lookup[(chunk >> 12) & 0x3F]);
                buf.append(lookup[(chunk >> 6) & 0x3F]);
            }
            else { // 1 byte
                buf.append(lookup[(chunk >> 12) & 0x3F]);
                buf.append('=');
            }
            buf.append('=');
        }
        return buf.toString();
    }

    /**
     * Decodes the specified base64 string back into its raw data.
     *
     * @param encoded The base64 encoded string.
     */
    public static byte[] base64Decode(String encoded)
    {
        int padding = 0;

        for (int i = encoded.length() - 1; encoded.charAt(i) == '='; i--) {
            padding++;
        }

        int length = encoded.length() * 6 / 8 - padding;
        byte[] bytes = new byte[length];

        for (int i = 0, index = 0, n = encoded.length(); i < n; i += 4) {
            int word = reverseLookup[encoded.charAt(i)] << 18;
            word += reverseLookup[encoded.charAt(i + 1)] << 12;
            word += reverseLookup[encoded.charAt(i + 2)] << 6;
            word += reverseLookup[encoded.charAt(i + 3)];

            for (int j = 0; j < 3 && index + j < length; j++) {
                bytes[index + j] = (byte) (word >> (8 * (2 - j)));
            }

            index += 3;
        }

        return bytes;
    }
}


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

import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * An OutputStream that writes contents to a Logger upon each call to flush()
 */
class LoggingOutputStream
        extends ByteArrayOutputStream
{
    private final String lineSeparator;
    private final Logger logger;

    public LoggingOutputStream(Logger logger)
    {
        this.logger = logger;
        lineSeparator = System.getProperty("line.separator");
    }

    /**
     * write the current buffer contents to the underlying logger.
     */
    @Override
    public synchronized void flush()
            throws IOException
    {
        super.flush();
        String record = this.toString();
        reset();

        if (record.isEmpty() || record.equals(lineSeparator)) {
            // avoid empty records
            return;
        }

        logger.info(record);
    }
}