/*
 * Copyright (c) 2010-2013 the original author or authors
 *
 * 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.
 *
 */
package org.jmxtrans.agent.google;

import org.jmxtrans.agent.util.json.Json;
import org.jmxtrans.agent.util.json.JsonArray;
import org.jmxtrans.agent.util.json.JsonObject;
import org.jmxtrans.agent.util.logging.Logger;

import javax.annotation.Nonnull;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

/**
 * @author Evgeny Minkevich
 * @author Mitch Simpson
 */
public class ApiFacade {

    private static final Logger logger = Logger.getLogger(ApiFacade.class.getName());
    private static String projectId = null;

    private static Connection cf;

    static Boolean initConnection(String project, String account, String accountKey, String credentialsFile) {
        projectId = project;
        try {
            cf = new Connection(account, accountKey, credentialsFile);
        } catch (Exception e) {
            logger.log(Level.WARNING, "Unable to initialise metric service connection", e);
            return false;
        }
        return true;
    }

    static void uploadMetric(String metricName, String start, String end, Object value, Map<String, String> labelValues) {

        String request = "projects/" + projectId + "/timeSeries";

        JsonObject root = new JsonObject();
        JsonArray timeSeries = new JsonArray();
        root.add("timeSeries", timeSeries);

        JsonObject series0 = new JsonObject();
        timeSeries.add(series0);

        series0.add("resource", new JsonObject().add("type", "global"));

        JsonObject metric = new JsonObject();
        metric.add("type", "custom.googleapis.com/" + metricName);

        JsonObject labels = new JsonObject();
        for (Map.Entry<String, String> labelEntry : labelValues.entrySet()) {
            labels.add(labelEntry.getKey(), labelEntry.getValue());
        }

        metric.add("labels", labels);
        series0.add("metric", metric);

        JsonArray points = new JsonArray();
        points.add(getPoint(start, end, value));

        series0.add("points", points);

        try {
            cf.doPost(request, root.toString());
        } catch (Exception e) {
            logger.log(Level.WARNING, "Failed to upload metric value for : " + metricName + " : " + value, e);
        }
    }

    private static JsonObject getPoint(String start, String end, Object value) {
        JsonObject point = new JsonObject();

        JsonObject interval = new JsonObject();
        interval.add("startTime", start);
        interval.add("endTime", end);
        point.add("interval", interval);

        JsonObject val = new JsonObject();

        if (value instanceof Double || value instanceof Float) {
            val.add("doubleValue", (Double) value);
        } else if (value instanceof Long || value instanceof Integer) {
            val.add("int64Value", "" + value);
        } else if (value instanceof Boolean) {
            val.add("boolValue", (Boolean) value);
        } else if (value instanceof String) {
            val.add("stringValue", (String) value);
        } else {
            val.add("stringValue", value.toString());
        }

        point.add("value", val);

        return point;
    }

    static String initCustomMetricDescriptor(
            @Nonnull String metricType,
            @Nonnull String metricKind,
            @Nonnull String metricUnit,
            @Nonnull String valueType,
            @Nonnull List<String> classificationLabels
    ) {
        try {
            String customMetricType = "custom.googleapis.com/" + metricType;

            if (checkMetricDescriptorExists(projectId, customMetricType)) {
                logger.log(Level.INFO, "Existing custom metric found : " + customMetricType);
                return metricKind;
            }

            createCustomMetricDescriptor(
                    metricType,
                    metricKind,
                    metricUnit,
                    valueType,
                    classificationLabels);

            return metricKind;
        } catch (Exception e) {
            logger.log(Level.WARNING, "Failed to initialise metric : " + metricType, e);
            return "FAIL";
        }
    }

    private static boolean checkMetricDescriptorExists(String project, String metricName) {
        String request = "projects/" + project + "/metricDescriptors/" + metricName;
        try {
            String response = cf.doGet(request, null);
            logger.fine("Metric received : " + response);
            JsonObject obj = Json.parse(response).asObject();
            if (obj != null &&
                    obj.get("name") != null &&
                    obj.get("name").asString().equalsIgnoreCase(request)) {
                return true;
            }
        } catch (RuntimeException e) {
            JsonObject error = Json.parse(e.getMessage()).asObject();
            if (error.get("error")!=null && error.get("error").asObject().get("code").asInt() == 404){
                logger.info("Metric '" + metricName + "' is not found in the project");
            } else {
                logger.log(Level.WARNING, "Unable to retrieve metric : " + e.getMessage(), e);
                logger.log(Level.WARNING, "Request : " + request);
            }
        } catch (Exception e) {
            logger.log(Level.WARNING, "Unable to retrieve metric : " + e.getMessage(), e);
            logger.log(Level.WARNING, "Request : " + request);
        }
        return false;
    }

    /**
     * This method creates a custom metric with arbitrary names, description,
     * and units.
     */
    private static void createCustomMetricDescriptor(
            @Nonnull String metricName,
            @Nonnull String metricKind,
            @Nonnull String metricUnit,
            @Nonnull String metricValueType,
            @Nonnull List<String> classificationLabels
    ) throws Exception {

        logger.info("Registering new custom metric : " + metricName + " : " + metricKind + " : " + metricUnit + " : " + metricValueType + " : " + classificationLabels);

        String url = "projects/" + projectId + "/metricDescriptors";
        String fullName = "custom.googleapis.com/" + metricName;

        JsonObject md = new JsonObject();
        md.add("name", url + "/" + URLEncoder.encode(fullName, "UTF-8"));
        md.add("type", fullName);

        md.add("metricKind", metricKind.toUpperCase());
        md.add("valueType", metricValueType.toUpperCase());
        md.add("unit", metricUnit);

        // Metric Labels
        JsonArray labels = new JsonArray();
        for (String entry : classificationLabels) {
            JsonObject label = new JsonObject();
            label.add("key", entry);
            label.add("description", entry);
            label.add("valueType", "STRING");
            labels.add(label);
        }
        md.add("labels", labels);

        String request = md.toString();
        logger.fine("Metric creation request : " + request);

        String response = cf.doPost(url, md.toString());
        logger.finer("Metric creation response" + response);

        logger.info("Created custom metric : " + metricName);
    }
}


/*******************************************************************************
 * Copyright (c) 2013, 2016 EclipseSource.
 *
 * 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.
 ******************************************************************************/
package org.jmxtrans.agent.util.json;

import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.Writer;

/**
 * Represents a JSON value. This can be a JSON object, an  array,
 * a number, a string, or one of the literals
 * true, false, and null.
 *

 * The literals true, false, and null are
 * represented by the constants {@link #TRUE}, {@link #FALSE}, and {@link #NULL}.
 *

 *

 * JSON objects and arrays are represented by the subtypes
 * {@link JsonObject} and {@link JsonArray}. Instances of these types can be created using the
 * public constructors of these classes.
 *

 *

 * Instances that represent JSON numbers, strings and
 * boolean values can be created using the static factory methods
 * {@link #valueOf(String)}, {@link #valueOf(long)}, {@link #valueOf(double)}, etc.
 *

 *

 * In order to find out whether an instance of this class is of a certain type, the methods
 * {@link #isObject()}, {@link #isArray()}, {@link #isString()}, {@link #isNumber()} etc. can be
 * used.
 *

 *

 * If the type of a JSON value is known, the methods {@link #asObject()}, {@link #asArray()},
 * {@link #asString()}, {@link #asInt()}, etc. can be used to get this value directly in the
 * appropriate target type.
 *

 *

 * This class is not supposed to be extended by clients.
 *

 */
@SuppressWarnings("serial") // use default serial UID
public abstract class JsonValue implements Serializable {

  /**
   * Represents the JSON literal true.
   * @deprecated Use Json.TRUE instead
   */
  @Deprecated
  public static final JsonValue TRUE = new JsonLiteral("true");

  /**
   * Represents the JSON literal false.
   * @deprecated Use Json.FALSE instead
   */
  @Deprecated
  public static final JsonValue FALSE = new JsonLiteral("false");

  /**
   * Represents the JSON literal null.
   * @deprecated Use Json.NULL instead
   */
  @Deprecated
  public static final JsonValue NULL = new JsonLiteral("null");

  JsonValue() {
    // prevent subclasses outside of this package
  }

  /**
   * Reads a JSON value from the given reader.
   *

   * Characters are read in chunks and buffered internally, therefore wrapping an existing reader in
   * an additional BufferedReader does not improve reading
   * performance.
   *

   *
   * @param reader
   *          the reader to read the JSON value from
   * @return the JSON value that has been read
   * @throws IOException
   *           if an I/O error occurs in the reader
   * @throws ParseException
   *           if the input is not valid JSON
   * @deprecated Use {@link Json#parse(Reader)} instead
   */
  @Deprecated
  public static JsonValue readFrom(Reader reader) throws IOException {
    return Json.parse(reader);
  }

  /**
   * Reads a JSON value from the given string.
   *
   * @param text
   *          the string that contains the JSON value
   * @return the JSON value that has been read
   * @throws ParseException
   *           if the input is not valid JSON
   * @deprecated Use {@link Json#parse(String)} instead
   */
  @Deprecated
  public static JsonValue readFrom(String text) {
    return Json.parse(text);
  }

  /**
   * Returns a JsonValue instance that represents the given int value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   * @deprecated Use Json.value() instead
   */
  @Deprecated
  public static JsonValue valueOf(int value) {
    return Json.value(value);
  }

  /**
   * Returns a JsonValue instance that represents the given long value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   * @deprecated Use Json.value() instead
   */
  @Deprecated
  public static JsonValue valueOf(long value) {
    return Json.value(value);
  }

  /**
   * Returns a JsonValue instance that represents the given float value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   * @deprecated Use Json.value() instead
   */
  @Deprecated
  public static JsonValue valueOf(float value) {
    return Json.value(value);
  }

  /**
   * Returns a JsonValue instance that represents the given double value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   * @deprecated Use Json.value() instead
   */
  @Deprecated
  public static JsonValue valueOf(double value) {
    return Json.value(value);
  }

  /**
   * Returns a JsonValue instance that represents the given string.
   *
   * @param string
   *          the string to get a JSON representation for
   * @return a JSON value that represents the given string
   * @deprecated Use Json.value() instead
   */
  @Deprecated
  public static JsonValue valueOf(String string) {
    return Json.value(string);
  }

  /**
   * Returns a JsonValue instance that represents the given boolean value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   * @deprecated Use Json.value() instead
   */
  @Deprecated
  public static JsonValue valueOf(boolean value) {
    return Json.value(value);
  }

  /**
   * Detects whether this value represents a JSON object. If this is the case, this value is an
   * instance of {@link JsonObject}.
   *
   * @return true if this value is an instance of JsonObject
   */
  public boolean isObject() {
    return false;
  }

  /**
   * Detects whether this value represents a JSON array. If this is the case, this value is an
   * instance of {@link JsonArray}.
   *
   * @return true if this value is an instance of JsonArray
   */
  public boolean isArray() {
    return false;
  }

  /**
   * Detects whether this value represents a JSON number.
   *
   * @return true if this value represents a JSON number
   */
  public boolean isNumber() {
    return false;
  }

  /**
   * Detects whether this value represents a JSON string.
   *
   * @return true if this value represents a JSON string
   */
  public boolean isString() {
    return false;
  }

  /**
   * Detects whether this value represents a boolean value.
   *
   * @return true if this value represents either the JSON literal true or
   *         false
   */
  public boolean isBoolean() {
    return false;
  }

  /**
   * Detects whether this value represents the JSON literal true.
   *
   * @return true if this value represents the JSON literal true
   */
  public boolean isTrue() {
    return false;
  }

  /**
   * Detects whether this value represents the JSON literal false.
   *
   * @return true if this value represents the JSON literal false
   */
  public boolean isFalse() {
    return false;
  }

  /**
   * Detects whether this value represents the JSON literal null.
   *
   * @return true if this value represents the JSON literal null
   */
  public boolean isNull() {
    return false;
  }

  /**
   * Returns this JSON value as {@link JsonObject}, assuming that this value represents a JSON
   * object. If this is not the case, an exception is thrown.
   *
   * @return a JSONObject for this value
   * @throws UnsupportedOperationException
   *           if this value is not a JSON object
   */
  public JsonObject asObject() {
    throw new UnsupportedOperationException("Not an object: " + toString());
  }

  /**
   * Returns this JSON value as {@link JsonArray}, assuming that this value represents a JSON array.
   * If this is not the case, an exception is thrown.
   *
   * @return a JSONArray for this value
   * @throws UnsupportedOperationException
   *           if this value is not a JSON array
   */
  public JsonArray asArray() {
    throw new UnsupportedOperationException("Not an array: " + toString());
  }

  /**
   * Returns this JSON value as an int value, assuming that this value represents a
   * JSON number that can be interpreted as Java int. If this is not the case, an
   * exception is thrown.
   *

   * To be interpreted as Java int, the JSON number must neither contain an exponent
   * nor a fraction part. Moreover, the number must be in the Integer range.
   *

   *
   * @return this value as int
   * @throws UnsupportedOperationException
   *           if this value is not a JSON number
   * @throws NumberFormatException
   *           if this JSON number can not be interpreted as int value
   */
  public int asInt() {
    throw new UnsupportedOperationException("Not a number: " + toString());
  }

  /**
   * Returns this JSON value as a long value, assuming that this value represents a
   * JSON number that can be interpreted as Java long. If this is not the case, an
   * exception is thrown.
   *

   * To be interpreted as Java long, the JSON number must neither contain an exponent
   * nor a fraction part. Moreover, the number must be in the Long range.
   *

   *
   * @return this value as long
   * @throws UnsupportedOperationException
   *           if this value is not a JSON number
   * @throws NumberFormatException
   *           if this JSON number can not be interpreted as long value
   */
  public long asLong() {
    throw new UnsupportedOperationException("Not a number: " + toString());
  }

  /**
   * Returns this JSON value as a float value, assuming that this value represents a
   * JSON number. If this is not the case, an exception is thrown.
   *

   * If the JSON number is out of the Float range, {@link Float#POSITIVE_INFINITY} or
   * {@link Float#NEGATIVE_INFINITY} is returned.
   *

   *
   * @return this value as float
   * @throws UnsupportedOperationException
   *           if this value is not a JSON number
   */
  public float asFloat() {
    throw new UnsupportedOperationException("Not a number: " + toString());
  }

  /**
   * Returns this JSON value as a double value, assuming that this value represents a
   * JSON number. If this is not the case, an exception is thrown.
   *

   * If the JSON number is out of the Double range, {@link Double#POSITIVE_INFINITY} or
   * {@link Double#NEGATIVE_INFINITY} is returned.
   *

   *
   * @return this value as double
   * @throws UnsupportedOperationException
   *           if this value is not a JSON number
   */
  public double asDouble() {
    throw new UnsupportedOperationException("Not a number: " + toString());
  }

  /**
   * Returns this JSON value as String, assuming that this value represents a JSON string. If this
   * is not the case, an exception is thrown.
   *
   * @return the string represented by this value
   * @throws UnsupportedOperationException
   *           if this value is not a JSON string
   */
  public String asString() {
    throw new UnsupportedOperationException("Not a string: " + toString());
  }

  /**
   * Returns this JSON value as a boolean value, assuming that this value is either
   * true or false. If this is not the case, an exception is thrown.
   *
   * @return this value as boolean
   * @throws UnsupportedOperationException
   *           if this value is neither true or false
   */
  public boolean asBoolean() {
    throw new UnsupportedOperationException("Not a boolean: " + toString());
  }

  /**
   * Writes the JSON representation of this value to the given writer in its minimal form, without
   * any additional whitespace.
   *

   * Writing performance can be improved by using a {@link java.io.BufferedWriter BufferedWriter}.
   *

   *
   * @param writer
   *          the writer to write this value to
   * @throws IOException
   *           if an I/O error occurs in the writer
   */
  public void writeTo(Writer writer) throws IOException {
    writeTo(writer, WriterConfig.MINIMAL);
  }

  /**
   * Writes the JSON representation of this value to the given writer using the given formatting.
   *

   * Writing performance can be improved by using a {@link java.io.BufferedWriter BufferedWriter}.
   *

   *
   * @param writer
   *          the writer to write this value to
   * @param config
   *          a configuration that controls the formatting or null for the minimal form
   * @throws IOException
   *           if an I/O error occurs in the writer
   */
  public void writeTo(Writer writer, WriterConfig config) throws IOException {
    if (writer == null) {
      throw new NullPointerException("writer is null");
    }
    if (config == null) {
      throw new NullPointerException("config is null");
    }
    WritingBuffer buffer = new WritingBuffer(writer, 128);
    write(config.createWriter(buffer));
    buffer.flush();
  }

  /**
   * Returns the JSON string for this value in its minimal form, without any additional whitespace.
   * The result is guaranteed to be a valid input for the method {@link #readFrom(String)} and to
   * create a value that is equal to this object.
   *
   * @return a JSON string that represents this value
   */
  @Override
  public String toString() {
    return toString(WriterConfig.MINIMAL);
  }

  /**
   * Returns the JSON string for this value using the given formatting.
   *
   * @param config
   *          a configuration that controls the formatting or null for the minimal form
   * @return a JSON string that represents this value
   */
  public String toString(WriterConfig config) {
    StringWriter writer = new StringWriter();
    try {
      writeTo(writer, config);
    } catch (IOException exception) {
      // StringWriter does not throw IOExceptions
      throw new RuntimeException(exception);
    }
    return writer.toString();
  }

  /**
   * Indicates whether some other object is "equal to" this one according to the contract specified
   * in {@link Object#equals(Object)}.
   *

   * Two JsonValues are considered equal if and only if they represent the same JSON text. As a
   * consequence, two given JsonObjects may be different even though they contain the same set of
   * names with the same values, but in a different order.
   *

   *
   * @param object
   *          the reference object with which to compare
   * @return true if this object is the same as the object argument; false otherwise
   */
  @Override
  public boolean equals(Object object) {
    return super.equals(object);
  }

  @Override
  public int hashCode() {
    return super.hashCode();
  }

  abstract void write(JsonWriter writer) throws IOException;

}


/*******************************************************************************
 * Copyright (c) 2015, 2016 EclipseSource.
 *
 * 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.
 ******************************************************************************/
package org.jmxtrans.agent.util.json;

import java.io.IOException;
import java.io.Reader;

/**
 * This class serves as the entry point to the minimal-json API.
 *

 * To parse a given JSON input, use the parse() methods like in this
 * example:
 *

 *

 * JsonObject object = Json.parse(string).asObject();
 *

 *

 * To create a JSON data structure to be serialized, use the methods
 * value(), array(), and object(). For example, the following
 * snippet will produce the JSON string {"foo": 23, "bar": true}:
 *

 *

 * String string = Json.object().add("foo", 23).add("bar", true).toString();
 *

 *

 * To create a JSON array from a given Java array, you can use one of the array()
 * methods with varargs parameters:
 *

 *

 * String[] names = ...
 * JsonArray array = Json.array(names);
 *

 */
public final class Json {

  private Json() {
    // not meant to be instantiated
  }

  /**
   * Represents the JSON literal null.
   */
  public static final org.jmxtrans.agent.util.json.JsonValue NULL = new JsonLiteral("null");

  /**
   * Represents the JSON literal true.
   */
  public static final org.jmxtrans.agent.util.json.JsonValue TRUE = new JsonLiteral("true");

  /**
   * Represents the JSON literal false.
   */
  public static final org.jmxtrans.agent.util.json.JsonValue FALSE = new JsonLiteral("false");

  /**
   * Returns a JsonValue instance that represents the given int value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static org.jmxtrans.agent.util.json.JsonValue value(int value) {
    return new org.jmxtrans.agent.util.json.JsonNumber(Integer.toString(value, 10));
  }

  /**
   * Returns a JsonValue instance that represents the given long value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static org.jmxtrans.agent.util.json.JsonValue value(long value) {
    return new org.jmxtrans.agent.util.json.JsonNumber(Long.toString(value, 10));
  }

  /**
   * Returns a JsonValue instance that represents the given float value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static org.jmxtrans.agent.util.json.JsonValue value(float value) {
    if (Float.isInfinite(value) || Float.isNaN(value)) {
      throw new IllegalArgumentException("Infinite and NaN values not permitted in JSON");
    }
    return new org.jmxtrans.agent.util.json.JsonNumber(cutOffPointZero(Float.toString(value)));
  }

  /**
   * Returns a JsonValue instance that represents the given double value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static org.jmxtrans.agent.util.json.JsonValue value(double value) {
    if (Double.isInfinite(value) || Double.isNaN(value)) {
      throw new IllegalArgumentException("Infinite and NaN values not permitted in JSON");
    }
    return new org.jmxtrans.agent.util.json.JsonNumber(cutOffPointZero(Double.toString(value)));
  }

  /**
   * Returns a JsonValue instance that represents the given string.
   *
   * @param string
   *          the string to get a JSON representation for
   * @return a JSON value that represents the given string
   */
  public static org.jmxtrans.agent.util.json.JsonValue value(String string) {
    return string == null ? NULL : new JsonString(string);
  }

  /**
   * Returns a JsonValue instance that represents the given boolean value.
   *
   * @param value
   *          the value to get a JSON representation for
   * @return a JSON value that represents the given value
   */
  public static org.jmxtrans.agent.util.json.JsonValue value(boolean value) {
    return value ? TRUE : FALSE;
  }

  /**
   * Creates a new empty JsonArray. This is equivalent to creating a new JsonArray using the
   * constructor.
   *
   * @return a new empty JSON array
   */
  public static JsonArray array() {
    return new JsonArray();
  }

  /**
   * Creates a new JsonArray that contains the JSON representations of the given int
   * values.
   *
   * @param values
   *          the values to be included in the new JSON array
   * @return a new JSON array that contains the given values
   */
  public static JsonArray array(int... values) {
    if (values == null) {
      throw new NullPointerException("values is null");
    }
    JsonArray array = new JsonArray();
    for (int value : values) {
      array.add(value);
    }
    return array;
  }

  /**
   * Creates a new JsonArray that contains the JSON representations of the given long
   * values.
   *
   * @param values
   *          the values to be included in the new JSON array
   * @return a new JSON array that contains the given values
   */
  public static JsonArray array(long... values) {
    if (values == null) {
      throw new NullPointerException("values is null");
    }
    JsonArray array = new JsonArray();
    for (long value : values) {
      array.add(value);
    }
    return array;
  }

  /**
   * Creates a new JsonArray that contains the JSON representations of the given float
   * values.
   *
   * @param values
   *          the values to be included in the new JSON array
   * @return a new JSON array that contains the given values
   */
  public static JsonArray array(float... values) {
    if (values == null) {
      throw new NullPointerException("values is null");
    }
    JsonArray array = new JsonArray();
    for (float value : values) {
      array.add(value);
    }
    return array;
  }

  /**
   * Creates a new JsonArray that contains the JSON representations of the given double
   * values.
   *
   * @param values
   *          the values to be included in the new JSON array
   * @return a new JSON array that contains the given values
   */
  public static JsonArray array(double... values) {
    if (values == null) {
      throw new NullPointerException("values is null");
    }
    JsonArray array = new JsonArray();
    for (double value : values) {
      array.add(value);
    }
    return array;
  }

  /**
   * Creates a new JsonArray that contains the JSON representations of the given
   * boolean values.
   *
   * @param values
   *          the values to be included in the new JSON array
   * @return a new JSON array that contains the given values
   */
  public static JsonArray array(boolean... values) {
    if (values == null) {
      throw new NullPointerException("values is null");
    }
    JsonArray array = new JsonArray();
    for (boolean value : values) {
      array.add(value);
    }
    return array;
  }

  /**
   * Creates a new JsonArray that contains the JSON representations of the given strings.
   *
   * @param strings
   *          the strings to be included in the new JSON array
   * @return a new JSON array that contains the given strings
   */
  public static JsonArray array(String... strings) {
    if (strings == null) {
      throw new NullPointerException("values is null");
    }
    JsonArray array = new JsonArray();
    for (String value : strings) {
      array.add(value);
    }
    return array;
  }

  /**
   * Creates a new empty JsonObject. This is equivalent to creating a new JsonObject using the
   * constructor.
   *
   * @return a new empty JSON object
   */
  public static org.jmxtrans.agent.util.json.JsonObject object() {
    return new org.jmxtrans.agent.util.json.JsonObject();
  }

  /**
   * Parses the given input string as JSON. The input must contain a valid JSON value, optionally
   * padded with whitespace.
   *
   * @param string
   *          the input string, must be valid JSON
   * @return a value that represents the parsed JSON
   * @throws ParseException
   *           if the input is not valid JSON
   */
  public static org.jmxtrans.agent.util.json.JsonValue parse(String string) {
    if (string == null) {
      throw new NullPointerException("string is null");
    }
    DefaultHandler handler = new DefaultHandler();
    new JsonParser(handler).parse(string);
    return handler.getValue();
  }

  /**
   * Reads the entire input from the given reader and parses it as JSON. The input must contain a
   * valid JSON value, optionally padded with whitespace.
   *

   * Characters are read in chunks into an input buffer. Hence, wrapping a reader in an additional
   * BufferedReader likely won't improve reading performance.
   *

   *
   * @param reader
   *          the reader to read the JSON value from
   * @return a value that represents the parsed JSON
   * @throws IOException
   *           if an I/O error occurs in the reader
   * @throws ParseException
   *           if the input is not valid JSON
   */
  public static org.jmxtrans.agent.util.json.JsonValue parse(Reader reader) throws IOException {
    if (reader == null) {
      throw new NullPointerException("reader is null");
    }
    DefaultHandler handler = new DefaultHandler();
    new JsonParser(handler).parse(reader);
    return handler.getValue();
  }

  private static String cutOffPointZero(String string) {
    if (string.endsWith(".0")) {
      return string.substring(0, string.length() - 2);
    }
    return string;
  }

  static class DefaultHandler extends JsonHandler<JsonArray, org.jmxtrans.agent.util.json.JsonObject> {

    protected org.jmxtrans.agent.util.json.JsonValue value;

    @Override
    public JsonArray startArray() {
      return new JsonArray();
    }

    @Override
    public org.jmxtrans.agent.util.json.JsonObject startObject() {
      return new org.jmxtrans.agent.util.json.JsonObject();
    }

    @Override
    public void endNull() {
      value = NULL;
    }

    @Override
    public void endBoolean(boolean bool) {
      value = bool ? TRUE : FALSE;
    }

    @Override
    public void endString(String string) {
      value = new JsonString(string);
    }

    @Override
    public void endNumber(String string) {
      value = new org.jmxtrans.agent.util.json.JsonNumber(string);
    }

    @Override
    public void endArray(JsonArray array) {
      value = array;
    }

    @Override
    public void endObject(org.jmxtrans.agent.util.json.JsonObject object) {
      value = object;
    }

    @Override
    public void endArrayValue(JsonArray array) {
      array.add(value);
    }

    @Override
    public void endObjectValue(org.jmxtrans.agent.util.json.JsonObject object, String name) {
      object.add(name, value);
    }

    org.jmxtrans.agent.util.json.JsonValue getValue() {
      return value;
    }

  }

}


/*
 * Copyright (c) 2010-2013 the original author or authors
 *
 * 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.
 *
 */
package org.jmxtrans.agent.util;

import java.util.Map;

/**
 * @author Cyrille Le Clerc
 */
public class ConfigurationUtils {
    private ConfigurationUtils() {

    }

    /**
     * Convert value of this setting to a Java int.
     *
     * If the setting is not found or is not an int, an exception is thrown.
     *
     * @param name name of the setting / property
     * @return int value of the setting / property
     * @throws IllegalArgumentException if setting is not found or is not an integer.
     */
    public static int getInt(Map<String, String> settings, String name) throws IllegalArgumentException {
        String value = getString(settings, name);
        try {
            return Integer.parseInt(value);
        } catch (Exception e) {
            throw new IllegalArgumentException("Setting '" + name + "=" + value + "' is not an integer on " + settings);
        }
    }

    /**
     * Convert value of this setting to a Java int.
     *
     * If the property is not found, the defaultValue is returned. If the property is not an int, an exception is thrown.
     *
     * @param name         name of the property
     * @param defaultValue default value if the property is not defined.
     * @return int value of the property or defaultValue if the property is not defined.
     * @throws IllegalArgumentException if setting is not is not an integer.
     */
    public static int getInt(Map<String, String> settings, String name, int defaultValue) throws IllegalArgumentException {
        if (settings.containsKey(name)) {

            String value = settings.get(name);
            try {
                return Integer.parseInt(value);
            } catch (Exception e) {
                throw new IllegalArgumentException("Setting '" + name + "=" + value + "' is not an integer on " + settings);
            }
        } else {
            return defaultValue;
        }
    }

    /**
     * Convert value of this setting to a Java long.
     *
     * If the property is not found, the defaultValue is returned. If the property is not a long, an exception is thrown.
     *
     * @param name         name of the property
     * @param defaultValue default value if the property is not defined.
     * @return int value of the property or defaultValue if the property is not defined.
     * @throws IllegalArgumentException if setting is not is not a long.
     */
    public static long getLong(Map<String, String> settings, String name, long defaultValue) throws IllegalArgumentException {
        if (settings.containsKey(name)) {

            String value = settings.get(name);
            try {
                return Long.parseLong(value);
            } catch (Exception e) {
                throw new IllegalArgumentException("Setting '" + name + "=" + value + "' is not a long on " + settings);
            }
        } else {
            return defaultValue;
        }
    }

    /**
     * Convert value of this setting to a Java boolean (via {@link Boolean#parseBoolean(String)}).
     *
     * If the property is not found, the defaultValue is returned.
     *
     * @param name         name of the property
     * @param defaultValue default value if the property is not defined.
     * @return int value of the property or defaultValue if the property is not defined.
     */
    public static boolean getBoolean(Map<String, String> settings, String name, boolean defaultValue) {
        if (settings.containsKey(name)) {

            String value = settings.get(name);
            return Boolean.parseBoolean(value);

        } else {
            return defaultValue;
        }
    }

    /**
     * Convert value of this setting to a Java int.
     *
     * If the setting is not found, an exception is thrown.
     *
     * @param name name of the property
     * @return value of the property
     * @throws IllegalArgumentException if setting is not found.
     */
    public static String getString(Map<String, String> settings, String name) throws IllegalArgumentException {
        if (!settings.containsKey(name)) {
            throw new IllegalArgumentException("No setting '" + name + "' found");
        }
        return settings.get(name);
    }

    /**
     * Return the value of the given property.
     *
     * If the property is not found, the defaultValue is returned.
     *
     * @param name         name of the property
     * @param defaultValue default value if the property is not defined.
     * @return value of the property or defaultValue if the property is not defined.
     */
    public static String getString(Map<String, String> settings, String name, String defaultValue) {
        if (settings.containsKey(name)) {
            return settings.get(name);
        } else {
            return defaultValue;
        }
    }
}


/*******************************************************************************
 * Copyright (c) 2013, 2015 EclipseSource.
 *
 * 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.
 ******************************************************************************/
package org.jmxtrans.agent.util.json;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.jmxtrans.agent.util.json.JsonObject.Member;

/**
 * Represents a JSON object, a set of name/value pairs, where the names are strings and the values
 * are JSON values.
 *

 * Members can be added using the add(String, ...) methods which accept instances of
 * {@link org.jmxtrans.agent.util.json.JsonValue}, strings, primitive numbers, and boolean values. To modify certain values of an
 * object, use the set(String, ...) methods. Please note that the add
 * methods are faster than set as they do not search for existing members. On the other
 * hand, the add methods do not prevent adding multiple members with the same name.
 * Duplicate names are discouraged but not prohibited by JSON.
 *

 *

 * Members can be accessed by their name using {@link #get(String)}. A list of all names can be
 * obtained from the method {@link #names()}. This class also supports iterating over the members in
 * document order using an {@link #iterator()} or an enhanced for loop:
 *

 *

 * for (Member member : jsonObject) {
 *   String name = member.getName();
 *   JsonValue value = member.getValue();
 *   ...
 * }
 *

 *

 * Even though JSON objects are unordered by definition, instances of this class preserve the order
 * of members to allow processing in document order and to guarantee a predictable output.
 *

 *

 * Note that this class is not thread-safe. If multiple threads access a
 * JsonObject instance concurrently, while at least one of these threads modifies the
 * contents of this object, access to the instance must be synchronized externally. Failure to do so
 * may lead to an inconsistent state.
 *

 *

 * This class is not supposed to be extended by clients.
 *

 */
public class JsonObject extends org.jmxtrans.agent.util.json.JsonValue implements Iterable<Member> {

  private static final long serialVersionUID = 19981017L;

  private final List<String> names;
  private final List<org.jmxtrans.agent.util.json.JsonValue> values;
  private transient HashIndexTable table;

  /**
   * Creates a new empty JsonObject.
   */
  public JsonObject() {
    names = new ArrayList<String>();
    values = new ArrayList<org.jmxtrans.agent.util.json.JsonValue>();
    table = new HashIndexTable();
  }

  /**
   * Creates a new JsonObject, initialized with the contents of the specified JSON object.
   *
   * @param object
   *          the JSON object to get the initial contents from, must not be null
   */
  public JsonObject(JsonObject object) {
    this(object, false);
  }

  private JsonObject(JsonObject object, boolean unmodifiable) {
    if (object == null) {
      throw new NullPointerException("object is null");
    }
    if (unmodifiable) {
      names = Collections.unmodifiableList(object.names);
      values = Collections.unmodifiableList(object.values);
    } else {
      names = new ArrayList<String>(object.names);
      values = new ArrayList<org.jmxtrans.agent.util.json.JsonValue>(object.values);
    }
    table = new HashIndexTable();
    updateHashIndex();
  }

  /**
   * Reads a JSON object from the given reader.
   *

   * Characters are read in chunks and buffered internally, therefore wrapping an existing reader in
   * an additional BufferedReader does not improve reading
   * performance.
   *

   *
   * @param reader
   *          the reader to read the JSON object from
   * @return the JSON object that has been read
   * @throws IOException
   *           if an I/O error occurs in the reader
   * @throws ParseException
   *           if the input is not valid JSON
   * @throws UnsupportedOperationException
   *           if the input does not contain a JSON object
   * @deprecated Use {@link Json#parse(Reader)}{@link org.jmxtrans.agent.util.json.JsonValue#asObject() .asObject()} instead
   */
  @Deprecated
  public static JsonObject readFrom(Reader reader) throws IOException {
    return org.jmxtrans.agent.util.json.JsonValue.readFrom(reader).asObject();
  }

  /**
   * Reads a JSON object from the given string.
   *
   * @param string
   *          the string that contains the JSON object
   * @return the JSON object that has been read
   * @throws ParseException
   *           if the input is not valid JSON
   * @throws UnsupportedOperationException
   *           if the input does not contain a JSON object
   * @deprecated Use {@link Json#parse(String)}{@link org.jmxtrans.agent.util.json.JsonValue#asObject() .asObject()} instead
   */
  @Deprecated
  public static JsonObject readFrom(String string) {
    return org.jmxtrans.agent.util.json.JsonValue.readFrom(string).asObject();
  }

  /**
   * Returns an unmodifiable JsonObject for the specified one. This method allows to provide
   * read-only access to a JsonObject.
   *

   * The returned JsonObject is backed by the given object and reflect changes that happen to it.
   * Attempts to modify the returned JsonObject result in an
   * UnsupportedOperationException.
   *

   *
   * @param object
   *          the JsonObject for which an unmodifiable JsonObject is to be returned
   * @return an unmodifiable view of the specified JsonObject
   */
  public static JsonObject unmodifiableObject(JsonObject object) {
    return new JsonObject(object, true);
  }

  /**
   * Appends a new member to the end of this object, with the specified name and the JSON
   * representation of the specified int value.
   *

   * This method does not prevent duplicate names. Calling this method with a name
   * that already exists in the object will append another member with the same name. In order to
   * replace existing members, use the method set(name, value) instead. However,
   *  add is much faster than set (because it does not need to
   * search for existing members). Therefore add should be preferred when constructing new
   * objects.
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject add(String name, int value) {
    add(name, Json.value(value));
    return this;
  }

  /**
   * Appends a new member to the end of this object, with the specified name and the JSON
   * representation of the specified long value.
   *

   * This method does not prevent duplicate names. Calling this method with a name
   * that already exists in the object will append another member with the same name. In order to
   * replace existing members, use the method set(name, value) instead. However,
   *  add is much faster than set (because it does not need to
   * search for existing members). Therefore add should be preferred when constructing new
   * objects.
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject add(String name, long value) {
    add(name, Json.value(value));
    return this;
  }

  /**
   * Appends a new member to the end of this object, with the specified name and the JSON
   * representation of the specified float value.
   *

   * This method does not prevent duplicate names. Calling this method with a name
   * that already exists in the object will append another member with the same name. In order to
   * replace existing members, use the method set(name, value) instead. However,
   *  add is much faster than set (because it does not need to
   * search for existing members). Therefore add should be preferred when constructing new
   * objects.
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject add(String name, float value) {
    add(name, Json.value(value));
    return this;
  }

  /**
   * Appends a new member to the end of this object, with the specified name and the JSON
   * representation of the specified double value.
   *

   * This method does not prevent duplicate names. Calling this method with a name
   * that already exists in the object will append another member with the same name. In order to
   * replace existing members, use the method set(name, value) instead. However,
   *  add is much faster than set (because it does not need to
   * search for existing members). Therefore add should be preferred when constructing new
   * objects.
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject add(String name, double value) {
    add(name, Json.value(value));
    return this;
  }

  /**
   * Appends a new member to the end of this object, with the specified name and the JSON
   * representation of the specified boolean value.
   *

   * This method does not prevent duplicate names. Calling this method with a name
   * that already exists in the object will append another member with the same name. In order to
   * replace existing members, use the method set(name, value) instead. However,
   *  add is much faster than set (because it does not need to
   * search for existing members). Therefore add should be preferred when constructing new
   * objects.
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject add(String name, boolean value) {
    add(name, Json.value(value));
    return this;
  }

  /**
   * Appends a new member to the end of this object, with the specified name and the JSON
   * representation of the specified string.
   *

   * This method does not prevent duplicate names. Calling this method with a name
   * that already exists in the object will append another member with the same name. In order to
   * replace existing members, use the method set(name, value) instead. However,
   *  add is much faster than set (because it does not need to
   * search for existing members). Therefore add should be preferred when constructing new
   * objects.
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject add(String name, String value) {
    add(name, Json.value(value));
    return this;
  }

  /**
   * Appends a new member to the end of this object, with the specified name and the specified JSON
   * value.
   *

   * This method does not prevent duplicate names. Calling this method with a name
   * that already exists in the object will append another member with the same name. In order to
   * replace existing members, use the method set(name, value) instead. However,
   *  add is much faster than set (because it does not need to
   * search for existing members). Therefore add should be preferred when constructing new
   * objects.
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add, must not be null
   * @return the object itself, to enable method chaining
   */
  public JsonObject add(String name, org.jmxtrans.agent.util.json.JsonValue value) {
    if (name == null) {
      throw new NullPointerException("name is null");
    }
    if (value == null) {
      throw new NullPointerException("value is null");
    }
    table.add(name, names.size());
    names.add(name);
    values.add(value);
    return this;
  }

  /**
   * Sets the value of the member with the specified name to the JSON representation of the
   * specified int value. If this object does not contain a member with this name, a
   * new member is added at the end of the object. If this object contains multiple members with
   * this name, only the last one is changed.
   *

   * This method should only be used to modify existing objects. To fill a new
   * object with members, the method add(name, value) should be preferred which is much
   * faster (as it does not need to search for existing members).
   *

   *
   * @param name
   *          the name of the member to replace
   * @param value
   *          the value to set to the member
   * @return the object itself, to enable method chaining
   */
  public JsonObject set(String name, int value) {
    set(name, Json.value(value));
    return this;
  }

  /**
   * Sets the value of the member with the specified name to the JSON representation of the
   * specified long value. If this object does not contain a member with this name, a
   * new member is added at the end of the object. If this object contains multiple members with
   * this name, only the last one is changed.
   *

   * This method should only be used to modify existing objects. To fill a new
   * object with members, the method add(name, value) should be preferred which is much
   * faster (as it does not need to search for existing members).
   *

   *
   * @param name
   *          the name of the member to replace
   * @param value
   *          the value to set to the member
   * @return the object itself, to enable method chaining
   */
  public JsonObject set(String name, long value) {
    set(name, Json.value(value));
    return this;
  }

  /**
   * Sets the value of the member with the specified name to the JSON representation of the
   * specified float value. If this object does not contain a member with this name, a
   * new member is added at the end of the object. If this object contains multiple members with
   * this name, only the last one is changed.
   *

   * This method should only be used to modify existing objects. To fill a new
   * object with members, the method add(name, value) should be preferred which is much
   * faster (as it does not need to search for existing members).
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject set(String name, float value) {
    set(name, Json.value(value));
    return this;
  }

  /**
   * Sets the value of the member with the specified name to the JSON representation of the
   * specified double value. If this object does not contain a member with this name, a
   * new member is added at the end of the object. If this object contains multiple members with
   * this name, only the last one is changed.
   *

   * This method should only be used to modify existing objects. To fill a new
   * object with members, the method add(name, value) should be preferred which is much
   * faster (as it does not need to search for existing members).
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject set(String name, double value) {
    set(name, Json.value(value));
    return this;
  }

  /**
   * Sets the value of the member with the specified name to the JSON representation of the
   * specified boolean value. If this object does not contain a member with this name,
   * a new member is added at the end of the object. If this object contains multiple members with
   * this name, only the last one is changed.
   *

   * This method should only be used to modify existing objects. To fill a new
   * object with members, the method add(name, value) should be preferred which is much
   * faster (as it does not need to search for existing members).
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject set(String name, boolean value) {
    set(name, Json.value(value));
    return this;
  }

  /**
   * Sets the value of the member with the specified name to the JSON representation of the
   * specified string. If this object does not contain a member with this name, a new member is
   * added at the end of the object. If this object contains multiple members with this name, only
   * the last one is changed.
   *

   * This method should only be used to modify existing objects. To fill a new
   * object with members, the method add(name, value) should be preferred which is much
   * faster (as it does not need to search for existing members).
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add
   * @return the object itself, to enable method chaining
   */
  public JsonObject set(String name, String value) {
    set(name, Json.value(value));
    return this;
  }

  /**
   * Sets the value of the member with the specified name to the specified JSON value. If this
   * object does not contain a member with this name, a new member is added at the end of the
   * object. If this object contains multiple members with this name, only the last one is changed.
   *

   * This method should only be used to modify existing objects. To fill a new
   * object with members, the method add(name, value) should be preferred which is much
   * faster (as it does not need to search for existing members).
   *

   *
   * @param name
   *          the name of the member to add
   * @param value
   *          the value of the member to add, must not be null
   * @return the object itself, to enable method chaining
   */
  public JsonObject set(String name, org.jmxtrans.agent.util.json.JsonValue value) {
    if (name == null) {
      throw new NullPointerException("name is null");
    }
    if (value == null) {
      throw new NullPointerException("value is null");
    }
    int index = indexOf(name);
    if (index != -1) {
      values.set(index, value);
    } else {
      table.add(name, names.size());
      names.add(name);
      values.add(value);
    }
    return this;
  }

  /**
   * Removes a member with the specified name from this object. If this object contains multiple
   * members with the given name, only the last one is removed. If this object does not contain a
   * member with the specified name, the object is not modified.
   *
   * @param name
   *          the name of the member to remove
   * @return the object itself, to enable method chaining
   */
  public JsonObject remove(String name) {
    if (name == null) {
      throw new NullPointerException("name is null");
    }
    int index = indexOf(name);
    if (index != -1) {
      table.remove(index);
      names.remove(index);
      values.remove(index);
    }
    return this;
  }

  /**
   * Copies all members of the specified object into this object. When the specified object contains
   * members with names that also exist in this object, the existing values in this object will be
   * replaced by the corresponding values in the specified object.
   *
   * @param object
   *          the object to merge
   * @return the object itself, to enable method chaining
   */
  public JsonObject merge(JsonObject object) {
    if (object == null) {
      throw new NullPointerException("object is null");
    }
    for (Member member : object) {
      this.set(member.name, member.value);
    }
    return this;
  }

  /**
   * Returns the value of the member with the specified name in this object. If this object contains
   * multiple members with the given name, this method will return the last one.
   *
   * @param name
   *          the name of the member whose value is to be returned
   * @return the value of the last member with the specified name, or null if this
   *         object does not contain a member with that name
   */
  public org.jmxtrans.agent.util.json.JsonValue get(String name) {
    if (name == null) {
      throw new NullPointerException("name is null");
    }
    int index = indexOf(name);
    return index != -1 ? values.get(index) : null;
  }

  /**
   * Returns the int value of the member with the specified name in this object. If
   * this object does not contain a member with this name, the given default value is returned. If
   * this object contains multiple members with the given name, the last one will be picked. If this
   * member's value does not represent a JSON number or if it cannot be interpreted as Java
   * int, an exception is thrown.
   *
   * @param name
   *          the name of the member whose value is to be returned
   * @param defaultValue
   *          the value to be returned if the requested member is missing
   * @return the value of the last member with the specified name, or the given default value if
   *         this object does not contain a member with that name
   */
  public int getInt(String name, int defaultValue) {
    org.jmxtrans.agent.util.json.JsonValue value = get(name);
    return value != null ? value.asInt() : defaultValue;
  }

  /**
   * Returns the long value of the member with the specified name in this object. If
   * this object does not contain a member with this name, the given default value is returned. If
   * this object contains multiple members with the given name, the last one will be picked. If this
   * member's value does not represent a JSON number or if it cannot be interpreted as Java
   * long, an exception is thrown.
   *
   * @param name
   *          the name of the member whose value is to be returned
   * @param defaultValue
   *          the value to be returned if the requested member is missing
   * @return the value of the last member with the specified name, or the given default value if
   *         this object does not contain a member with that name
   */
  public long getLong(String name, long defaultValue) {
    org.jmxtrans.agent.util.json.JsonValue value = get(name);
    return value != null ? value.asLong() : defaultValue;
  }

  /**
   * Returns the float value of the member with the specified name in this object. If
   * this object does not contain a member with this name, the given default value is returned. If
   * this object contains multiple members with the given name, the last one will be picked. If this
   * member's value does not represent a JSON number or if it cannot be interpreted as Java
   * float, an exception is thrown.
   *
   * @param name
   *          the name of the member whose value is to be returned
   * @param defaultValue
   *          the value to be returned if the requested member is missing
   * @return the value of the last member with the specified name, or the given default value if
   *         this object does not contain a member with that name
   */
  public float getFloat(String name, float defaultValue) {
    org.jmxtrans.agent.util.json.JsonValue value = get(name);
    return value != null ? value.asFloat() : defaultValue;
  }

  /**
   * Returns the double value of the member with the specified name in this object. If
   * this object does not contain a member with this name, the given default value is returned. If
   * this object contains multiple members with the given name, the last one will be picked. If this
   * member's value does not represent a JSON number or if it cannot be interpreted as Java
   * double, an exception is thrown.
   *
   * @param name
   *          the name of the member whose value is to be returned
   * @param defaultValue
   *          the value to be returned if the requested member is missing
   * @return the value of the last member with the specified name, or the given default value if
   *         this object does not contain a member with that name
   */
  public double getDouble(String name, double defaultValue) {
    org.jmxtrans.agent.util.json.JsonValue value = get(name);
    return value != null ? value.asDouble() : defaultValue;
  }

  /**
   * Returns the boolean value of the member with the specified name in this object. If
   * this object does not contain a member with this name, the given default value is returned. If
   * this object contains multiple members with the given name, the last one will be picked. If this
   * member's value does not represent a JSON true or false value, an
   * exception is thrown.
   *
   * @param name
   *          the name of the member whose value is to be returned
   * @param defaultValue
   *          the value to be returned if the requested member is missing
   * @return the value of the last member with the specified name, or the given default value if
   *         this object does not contain a member with that name
   */
  public boolean getBoolean(String name, boolean defaultValue) {
    org.jmxtrans.agent.util.json.JsonValue value = get(name);
    return value != null ? value.asBoolean() : defaultValue;
  }

  /**
   * Returns the String value of the member with the specified name in this object. If
   * this object does not contain a member with this name, the given default value is returned. If
   * this object contains multiple members with the given name, the last one is picked. If this
   * member's value does not represent a JSON string, an exception is thrown.
   *
   * @param name
   *          the name of the member whose value is to be returned
   * @param defaultValue
   *          the value to be returned if the requested member is missing
   * @return the value of the last member with the specified name, or the given default value if
   *         this object does not contain a member with that name
   */
  public String getString(String name, String defaultValue) {
    org.jmxtrans.agent.util.json.JsonValue value = get(name);
    return value != null ? value.asString() : defaultValue;
  }

  /**
   * Returns the number of members (name/value pairs) in this object.
   *
   * @return the number of members in this object
   */
  public int size() {
    return names.size();
  }

  /**
   * Returns true if this object contains no members.
   *
   * @return true if this object contains no members
   */
  public boolean isEmpty() {
    return names.isEmpty();
  }

  /**
   * Returns a list of the names in this object in document order. The returned list is backed by
   * this object and will reflect subsequent changes. It cannot be used to modify this object.
   * Attempts to modify the returned list will result in an exception.
   *
   * @return a list of the names in this object
   */
  public List<String> names() {
    return Collections.unmodifiableList(names);
  }

  /**
   * Returns an iterator over the members of this object in document order. The returned iterator
   * cannot be used to modify this object.
   *
   * @return an iterator over the members of this object
   */
  public Iterator<Member> iterator() {
    final Iterator<String> namesIterator = names.iterator();
    final Iterator<org.jmxtrans.agent.util.json.JsonValue> valuesIterator = values.iterator();
    return new Iterator<JsonObject.Member>() {

      public boolean hasNext() {
        return namesIterator.hasNext();
      }

      public Member next() {
        String name = namesIterator.next();
        org.jmxtrans.agent.util.json.JsonValue value = valuesIterator.next();
        return new Member(name, value);
      }

      public void remove() {
        throw new UnsupportedOperationException();
      }

    };
  }

  @Override
  void write(JsonWriter writer) throws IOException {
    writer.writeObjectOpen();
    Iterator<String> namesIterator = names.iterator();
    Iterator<org.jmxtrans.agent.util.json.JsonValue> valuesIterator = values.iterator();
    if (namesIterator.hasNext()) {
      writer.writeMemberName(namesIterator.next());
      writer.writeMemberSeparator();
      valuesIterator.next().write(writer);
      while (namesIterator.hasNext()) {
        writer.writeObjectSeparator();
        writer.writeMemberName(namesIterator.next());
        writer.writeMemberSeparator();
        valuesIterator.next().write(writer);
      }
    }
    writer.writeObjectClose();
  }

  @Override
  public boolean isObject() {
    return true;
  }

  @Override
  public JsonObject asObject() {
    return this;
  }

  @Override
  public int hashCode() {
    int result = 1;
    result = 31 * result + names.hashCode();
    result = 31 * result + values.hashCode();
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    JsonObject other = (JsonObject)obj;
    return names.equals(other.names) && values.equals(other.values);
  }

  int indexOf(String name) {
    int index = table.get(name);
    if (index != -1 && name.equals(names.get(index))) {
      return index;
    }
    return names.lastIndexOf(name);
  }

  private void readObject(ObjectInputStream inputStream)
      throws IOException, ClassNotFoundException
  {
    inputStream.defaultReadObject();
    table = new HashIndexTable();
    updateHashIndex();
  }

  private void updateHashIndex() {
    int size = names.size();
    for (int i = 0; i < size; i++) {
      table.add(names.get(i), i);
    }
  }

  /**
   * Represents a member of a JSON object, a pair of a name and a value.
   */
  public static class Member {

    private final String name;
    private final org.jmxtrans.agent.util.json.JsonValue value;

    Member(String name, org.jmxtrans.agent.util.json.JsonValue value) {
      this.name = name;
      this.value = value;
    }

    /**
     * Returns the name of this member.
     *
     * @return the name of this member, never null
     */
    public String getName() {
      return name;
    }

    /**
     * Returns the value of this member.
     *
     * @return the value of this member, never null
     */
    public org.jmxtrans.agent.util.json.JsonValue getValue() {
      return value;
    }

    @Override
    public int hashCode() {
      int result = 1;
      result = 31 * result + name.hashCode();
      result = 31 * result + value.hashCode();
      return result;
    }

    /**
     * Indicates whether a given object is "equal to" this JsonObject. An object is considered equal
     * if it is also a JsonObject and both objects contain the same members in
     * the same order.
     *

     * If two JsonObjects are equal, they will also produce the same JSON output.
     *

     *
     * @param object
     *          the object to be compared with this JsonObject
     * @return <tt>true</tt> if the specified object is equal to this JsonObject, false
     *         otherwise
     */
    @Override
    public boolean equals(Object object) {
      if (this == object) {
        return true;
      }
      if (object == null) {
        return false;
      }
      if (getClass() != object.getClass()) {
        return false;
      }
      Member other = (Member)object;
      return name.equals(other.name) && value.equals(other.value);
    }

  }

  static class HashIndexTable {

    private final byte[] hashTable = new byte[32]; // must be a power of two

    public HashIndexTable() {
    }

    public HashIndexTable(HashIndexTable original) {
      System.arraycopy(original.hashTable, 0, hashTable, 0, hashTable.length);
    }

    void add(String name, int index) {
      int slot = hashSlotFor(name);
      if (index < 0xff) {
        // increment by 1, 0 stands for empty
        hashTable[slot] = (byte)(index + 1);
      } else {
        hashTable[slot] = 0;
      }
    }

    void remove(int index) {
      for (int i = 0; i < hashTable.length; i++) {
        if (hashTable[i] == index + 1) {
          hashTable[i] = 0;
        } else if (hashTable[i] > index + 1) {
          hashTable[i]--;
        }
      }
    }

    int get(Object name) {
      int slot = hashSlotFor(name);
      // subtract 1, 0 stands for empty
      return (hashTable[slot] & 0xff) - 1;
    }

    private int hashSlotFor(Object element) {
      return element.hashCode() & hashTable.length - 1;
    }

  }

}


/*
 * Copyright (c) 2010-2013 the original author or authors
 *
 * 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.
 *
 */
package org.jmxtrans.agent.util;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * Inspired by Spring Property placeholder mechanism.
 *
 * Placeholders are delimited by '{' and '}' and support default value with ':'. Sample : '{graphite.host}'
 * or '{graphite.port:2003}'.
 *
 * If the placeholder is not found in the system properties, it is searched in the environment variables and then
 * converted to underscore delimited upper case and searched in environment variables.
 *
 * Sample for '{graphite.port:2003}':
 *

 *
System.getProperty("graphite.port")

 *
System.getenv("graphite.port")

 *
System.getenv("GRAPHITE_PORT")

 *
default to 2003

 *

 *
 * @author Cyrille Le Clerc
 */
public class PropertyPlaceholderResolver implements Serializable {

    private static final long serialVersionUID = 1L;
    private Map<String, String> externalProperties;

    public PropertyPlaceholderResolver(Map<String, String> externalProperties) {
        this.externalProperties = Objects.requireNonNull(externalProperties);
    }

    public PropertyPlaceholderResolver() {
        this(new HashMap<String, String>());
    }

    /**
     * Parse the given string resolving property placeholders (${my-property[:default-value]})
     *
     * @param string the string to parse.
     * @return the parsed string. Non null.
     * @throws IllegalStateException a property placeholder could not be resolved and no default value has been defined.
     */
    public String resolveString(String string) throws IllegalStateException {

        StringBuilder result = new StringBuilder(string.length());

        int position = 0;
        while (position < string.length()) {
            char c = string.charAt(position);
            if (c == '$' && (position < string.length() - 1) && string.charAt(position + 1) == '{') {
                int beginningBracketPosition = position + 1;
                int endingBracketPosition = string.indexOf('}', beginningBracketPosition);
                int colonPosition = string.indexOf(':', beginningBracketPosition);
                if (colonPosition > endingBracketPosition) {
                    colonPosition = -1;
                }

                String placeholder;
                String defaultValue;
                if (colonPosition == -1) {
                    placeholder = string.substring(beginningBracketPosition + 1, endingBracketPosition);
                    defaultValue = null;
                } else {
                    placeholder = string.substring(beginningBracketPosition + 1, colonPosition);
                    defaultValue = string.substring(colonPosition + 1, endingBracketPosition);
                }

                String resolvedPlaceHolder = resolvePlaceholder(placeholder, defaultValue);
                result.append(resolvedPlaceHolder);
                position = endingBracketPosition + 1;
            } else {
                result.append(c);
                position++;
            }
        }

        return result.toString();
    }

    /**
     * Search for the given placeholder in system properties then in environment variables.
     *
     * @param property     property to resolve
     * @param defaultValue Default value if the placeholder is not found. null means not default value is
     *                     defined and the placeholder must exist
     * @return the resolved property or the default value if the placeholder is not found and the default value is defined. Non null.
     * @throws IllegalStateException if the placeholder is not found and the given defaultValue is not
     *                              defined (null)
     */
    protected String resolvePlaceholder(String property, String defaultValue) throws IllegalStateException {

        // "graphite.host" -> "GRAPHITE_HOST"
        String environmentVariableStyleProperty = property.toUpperCase();
        environmentVariableStyleProperty = environmentVariableStyleProperty.replaceAll("\\.", "_");

        String result;
        if (externalProperties.containsKey(property)) {
            result = externalProperties.get(property);
        } else if (System.getProperties().containsKey(property)) {
            result = System.getProperty(property);
        } else if (System.getenv().containsKey(property)) {
            result = System.getenv(property);
        } else if (System.getenv().containsKey(environmentVariableStyleProperty)) {
            result = System.getenv(environmentVariableStyleProperty);
        } else if (defaultValue != null) {
            result = defaultValue;
        } else {
            throw new IllegalStateException("Property '" + property + "' not found in System properties nor in Environment variables");
        }
        return result;
    }

}


/*
 * Copyright (c) 2010-2013 the original author or authors
 *
 * 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.
 *
 */
package org.jmxtrans.agent.util.collect;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;

/**
 * Inspired by com.google.common.collect.Iterables.
 *
 * @author Cyrille Le Clerc
 */
public class Iterables2 {

    private Iterables2(){}

    /**
     * Returns the element at the specified position in an iterable.
     *
     * @param iterable the iterable to search into
     * @param position the position of the entry to return
     * @return the entry at the given location in the given iterable
     *         throws
     * @throws IndexOutOfBoundsException if given {@code position} is negative or
     *                                   greater than or equal to the size of given iterable
     */
    @Nullable
    public static <T> T get(@Nonnull Iterable<T> iterable, @Nonnegative int position) throws NullPointerException, IndexOutOfBoundsException {
        if (iterable == null)
            throw new NullPointerException("iterable");

        if (iterable instanceof List) {
            return ((List<T>) iterable).get(position);
        }

        if (position < 0)
            throw new IndexOutOfBoundsException("Requested position must be greater than 0, '" + position + "' is invalid");

        int idx = 0;
        for (T value : iterable) {
            if (idx == position) {
                return value;
            }
            idx++;
        }
        throw new IndexOutOfBoundsException("Requested position must be smaller than iterable size (" + idx + "), '" + position + "' is invalid");
    }
}


/*
 * Copyright (c) 2010-2013 the original author or authors
 *
 * 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.
 *
 */
package org.jmxtrans.agent;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import org.jmxtrans.agent.properties.NoPropertiesSourcePropertiesLoader;
import org.jmxtrans.agent.properties.PropertiesLoader;
import org.jmxtrans.agent.properties.ResourcePropertiesLoader;
import org.jmxtrans.agent.util.StringUtils2;
import org.jmxtrans.agent.util.io.Resource;
import org.jmxtrans.agent.util.io.ResourceFactory;
import org.jmxtrans.agent.util.logging.Logger;

import javax.annotation.Nonnull;
import javax.management.ObjectInstance;
import javax.management.ObjectName;

import java.lang.instrument.Instrumentation;
import java.lang.management.ManagementFactory;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

/**
 * @author Cyrille Le Clerc
 */
public class JmxTransAgent {
    private static Logger logger = Logger.getLogger(JmxTransAgent.class.getName());

    private static final String PROPERTIES_SYSTEM_PROPERTY_NAME = "jmxtrans.agent.properties.file";

    private JmxTransAgent(){}

    @SuppressFBWarnings("MS_SHOULD_BE_FINAL")
    public static boolean DIAGNOSTIC = Boolean.valueOf(System.getProperty(JmxTransAgent.class.getName() + ".diagnostic", "false"));

    public static void agentmain(String configFile, Instrumentation inst) {
        initializeAgent(configFile);
    }

    public static void premain(final String configFile, Instrumentation inst) {
        final int delayInSecs = Integer.parseInt(System.getProperty("jmxtrans.agent.premain.delay", "0"));
        final boolean waitForCustomMBeanServer =
            Boolean.parseBoolean(System.getProperty("jmxtrans.agent.premain.waitForCustomMBeanServer"));
        final int timeoutInSeconds = Integer.parseInt(
            System.getProperty("jmxtrans.agent.premain.waitForCustomMBeanServer.timeoutInSeconds", "120"));

        if (delayInSecs > 0) {
            logger.info("jmxtrans agent initialization delayed by " + delayInSecs + " seconds");
            new Thread("jmxtrans-agent-delayed-starter-" + delayInSecs + "secs") {
                @Override
                public void run() {
                    try {
                        Thread.sleep(delayInSecs * 1000);
                    } catch (InterruptedException e) {
                        Thread.interrupted();
                        return;
                    }

                    if (waitForCustomMBeanServer) {
                        logger.info("jmxtrans agent initialization delayed waiting for MBeanServer");

                        if (!waitForCustomMBeanServer(timeoutInSeconds)) {
                            return;
                        }
                    }

                    initializeAgent(configFile);
                }
            }.start();
        } else if (waitForCustomMBeanServer) {
            logger.info("jmxtrans agent initialization delayed waiting for custom MBeanServer");
            new Thread("jmxtrans-agent-delayed-starter-waitForCustomMBeanServer") {
                @Override
                public void run() {
                    if (!waitForCustomMBeanServer(timeoutInSeconds)) {
                        return;
                    }

                    initializeAgent(configFile);
                }
            }.start();
        } else {
            initializeAgent(configFile);
        }
    }

    public static void main(String[] args) {
        Logger.out.println(getVersionInfo());
    }

    /**
     * Human readable name and version.
     *
     * @return project name and version or "{@code jmxtrans-agent}" if package information not found
     */
    @Nonnull
    public static String getVersionInfo() {
        Package pkg = JmxTransAgent.class.getPackage();
        if (pkg == null) {
            return "jmxtrans-agent";
        } else {
            return pkg.getImplementationTitle() + ": " + pkg.getImplementationVersion();
        }
    }

    private static void initializeAgent(String configPath) {
        dumpDiagnosticInfo();
        if (configPath == null || configPath.isEmpty()) {
            String msg = "JmxTransExporter configurationFile must be defined";
            logger.log(Level.SEVERE, msg);
            throw new IllegalStateException(msg);
        }
        try {
            logger.info("Starting '" + getVersionInfo() + "' with configuration '" + configPath + "'...");

            PropertiesLoader propertiesLoader = createPropertiesLoader();
            Resource configuration = ResourceFactory.newResource(configPath);
            ExpressionLanguageEngine expressionLanguageEngine = new ExpressionLanguageEngineImpl();
            JmxTransConfigurationLoader configurationLoader = new JmxTransConfigurationXmlLoader(configuration, propertiesLoader, expressionLanguageEngine);
            JmxTransExporter jmxTransExporter = new JmxTransExporter(configurationLoader);
            //START
            jmxTransExporter.start();
            logger.info("JmxTransAgent started with configuration '" + configPath + "'");
        } catch (Exception e) {
            String msg = "Exception loading JmxTransExporter from '" + configPath + "'";
            logger.log(Level.SEVERE, msg, e);
            throw new IllegalStateException(msg, e);
        }
    }

    private static PropertiesLoader createPropertiesLoader() {
        String propertiesFile = System.getProperty(PROPERTIES_SYSTEM_PROPERTY_NAME);
        PropertiesLoader result;
        if (StringUtils2.isNullOrEmpty(propertiesFile)) {
            result = new NoPropertiesSourcePropertiesLoader();
        } else {
            result = new ResourcePropertiesLoader(propertiesFile);
        }
        logger.info("PropertiesLoader: " + result.getDescription());
        return result;
    }

    public static void dumpDiagnosticInfo() {
        if (!JmxTransAgent.DIAGNOSTIC)
            return;

        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                while (JmxTransAgent.DIAGNOSTIC) {

                    String prefix = new Timestamp(System.currentTimeMillis()) + " [jmxtrans-agent] ";
                    Logger.out.println(prefix + "JMXTRANS-AGENT DIAGNOSTIC INFO");

                    // CONTEXT
                    Logger.out.println(prefix + "Logger level: " + Logger.level);

                    // MBEANS
                    Set<ObjectInstance> objectInstances = ManagementFactory.getPlatformMBeanServer().queryMBeans(null, null);
                    List<ObjectName> objectNames = new ArrayList<>();
                    for (ObjectInstance objectInstance : objectInstances) {
                        objectNames.add(objectInstance.getObjectName());
                    }
                    Collections.sort(objectNames);
                    Logger.out.println(prefix + "ManagementFactory.getPlatformMBeanServer().queryMBeans(null, null)");
                    for (ObjectName objectName : objectNames) {
                        Logger.out.println(prefix + "\t" + objectName);
                    }

                    Logger.out.println(prefix + "ENF OF JMXTRANS-AGENT DIAGNOSING INFO");
                    try {
                        Thread.sleep(TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            }
        };

        Thread thread = new Thread(runnable);
        thread.setName("jmxtrans-agent-diagnostic");
        thread.setDaemon(true);
        thread.start();
    }

    /**
     * Polls every second to see if any {@link javax.management.MBeanServer} have been created
     * by another thread up to {@code timeoutInSeconds}. If interrupted or timed out, returns
     * {@code false}.
     * @param timeoutInSeconds Maximum number of seconds to wait before giving up.
     * @return {@code true} if found an {@code MBeanServer} within {@code timeoutInSeconds}.
     * {@code false} otherwise.
     */
    private static boolean waitForCustomMBeanServer(int timeoutInSeconds) {
        long startInMs = System.currentTimeMillis();

        while (!isCustomMBeanServerConfigured() && secondsSince(startInMs) < timeoutInSeconds) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.interrupted();
                return false;
            }
        }

        if (secondsSince(startInMs) >= timeoutInSeconds) {
            logger.info("jmxagent initialization timed out waiting for custom MBeanServer");
            return false;
        }

        return true;
    }

    /** @see javax.management.MBeanServerFactory */
    static private boolean isCustomMBeanServerConfigured() {
        return System.getProperty("javax.management.builder.initial") != null;
    }

    private static long secondsSince(long startInMs) {
        return (System.currentTimeMillis() - startInMs) / 1000;
    }
}


/*
 * Copyright (c) 2010-2013 the original author or authors
 *
 * 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.
 *
 */
package org.jmxtrans.agent.graphite;

import java.net.InetAddress;
import java.net.UnknownHostException;

import javax.annotation.Nullable;

/**
 * Generates metric messages to send to graphite instances.
 */
public class GraphiteMetricMessageBuilder {

    private static final String DEFAULT_METRIC_PATH_PREFIX_FORMAT = "servers.%s.";
    private final String metricPathPrefix;

    /**
     * @param configuredMetricPathPrefix
     *            Prefix to add to the metric keys. May be null, in which case servers.your_hostname will be used.
     */
    public GraphiteMetricMessageBuilder(@Nullable String configuredMetricPathPrefix) {
        this.metricPathPrefix = buildMetricPathPrefix(configuredMetricPathPrefix);
    }

    /**
     * Builds a metric string to send to a Graphite instance.
     *
     * @return The metric string without trailing newline
     */
    public String buildMessage(String metricName, Object value, long timestamp) {
        if (value instanceof Boolean) {
            return metricPathPrefix + metricName + " " + ((Boolean)value ? 1 : 0) + " " + timestamp;
        }
        return metricPathPrefix + metricName + " " + value + " " + timestamp;
    }

    /**
     * {@link java.net.InetAddress#getLocalHost()} may not be known at JVM startup when the process is launched as a Linux service.
     */
    private static String buildMetricPathPrefix(String configuredMetricPathPrefix) {
        if (configuredMetricPathPrefix != null) {
            return configuredMetricPathPrefix;
        }
        String hostname;
        try {
            hostname = InetAddress.getLocalHost().getHostName().replaceAll("\\.", "_");
        } catch (UnknownHostException e) {
            hostname = "#unknown#";
        }
        return String.format(DEFAULT_METRIC_PATH_PREFIX_FORMAT, hostname);
    }

    public String getPrefix() {
        return metricPathPrefix;
    }

}