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

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Properties;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;

import com.google.common.base.Strings;

import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient;
import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.log.Logger;

/**
 * A {@link org.apache.jmeter.samplers.Sampler Sampler} which produces Kafka messages.
 *
 * @author codyaray
 * @since 6/27/14
 *
 * @see "http://ilkinbalkanay.blogspot.com/2010/03/load-test-whatever-you-want-with-apache.html"
 * @see "http://newspaint.wordpress.com/2012/11/28/creating-a-java-sampler-for-jmeter/"
 * @see "http://jmeter.512774.n5.nabble.com/Custom-Sampler-Tutorial-td4490189.html"
 */
public class KafkaProducerSampler extends AbstractJavaSamplerClient {

  private static final Logger log = LoggingManager.getLoggerForClass();

  /**
   * Parameter for setting the Kafka brokers; for example, "kafka01:9092,kafka02:9092".
   */
  private static final String PARAMETER_KAFKA_BROKERS = "kafka_brokers";

  /**
   * Parameter for setting the Kafka topic name.
   */
  private static final String PARAMETER_KAFKA_TOPIC = "kafka_topic";

  /**
   * Parameter for setting the Kafka key.
   */
  private static final String PARAMETER_KAFKA_KEY = "kafka_key";

  /**
   * Parameter for setting the Kafka message.
   */
  private static final String PARAMETER_KAFKA_MESSAGE = "kafka_message";

  /**
   * Parameter for setting Kafka's {@code serializer.class} property.
   */
  private static final String PARAMETER_KAFKA_MESSAGE_SERIALIZER = "kafka_message_serializer";

  /**
   * Parameter for setting Kafka's {@code key.serializer.class} property.
   */
  private static final String PARAMETER_KAFKA_KEY_SERIALIZER = "kafka_key_serializer";

  /**
   * Parameter for setting the Kafka ssl keystore (include path information); for example, "server.keystore.jks".
   */
  private static final String PARAMETER_KAFKA_SSL_KEYSTORE = "kafka_ssl_keystore";

  /**
   * Parameter for setting the Kafka ssl keystore password.
   */
  private static final String PARAMETER_KAFKA_SSL_KEYSTORE_PASSWORD = "kafka_ssl_keystore_password";

  /**
   * Parameter for setting the Kafka ssl truststore (include path information); for example, "client.truststore.jks".
   */
  private static final String PARAMETER_KAFKA_SSL_TRUSTSTORE = "kafka_ssl_truststore";

  /**
   * Parameter for setting the Kafka ssl truststore password.
   */
  private static final String PARAMETER_KAFKA_SSL_TRUSTSTORE_PASSWORD = "kafka_ssl_truststore_password";

  /**
   * Parameter for setting the Kafka security protocol; "true" or "false".
   */
  private static final String PARAMETER_KAFKA_USE_SSL = "kafka_use_ssl";

  /**
   * Parameter for setting encryption. It is optional.
   */
  private static final String PARAMETER_KAFKA_COMPRESSION_TYPE = "kafka_compression_type";

  /**
   * Parameter for setting the partition. It is optional.
   */
  private static final String PARAMETER_KAFKA_PARTITION = "kafka_partition";

  //private Producer<Long, byte[]> producer;

  private KafkaProducer<String, String> producer;


  @Override
  public void setupTest(JavaSamplerContext context) {
    Properties props = new Properties();

    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, context.getParameter(PARAMETER_KAFKA_BROKERS));
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, context.getParameter(PARAMETER_KAFKA_KEY_SERIALIZER));
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, context.getParameter(PARAMETER_KAFKA_MESSAGE_SERIALIZER));
    props.put(ProducerConfig.ACKS_CONFIG, "1");

    // check if kafka security protocol is SSL or PLAINTEXT (default)
    if(context.getParameter(PARAMETER_KAFKA_USE_SSL).equals("true")){
      log.info("Setting up SSL properties...");
      props.put("security.protocol", "SSL");
      props.put("ssl.keystore.location", context.getParameter(PARAMETER_KAFKA_SSL_KEYSTORE));
      props.put("ssl.keystore.password", context.getParameter(PARAMETER_KAFKA_SSL_KEYSTORE_PASSWORD));
      props.put("ssl.truststore.location", context.getParameter(PARAMETER_KAFKA_SSL_TRUSTSTORE));
      props.put("ssl.truststore.password", context.getParameter(PARAMETER_KAFKA_SSL_TRUSTSTORE_PASSWORD));
    }

    String compressionType = context.getParameter(PARAMETER_KAFKA_COMPRESSION_TYPE);
    if (!Strings.isNullOrEmpty(compressionType)) {
      props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, compressionType);
    }

    producer = new KafkaProducer<String, String>(props);
  }

  @Override
  public void teardownTest(JavaSamplerContext context) {
    producer.close();
  }

  @Override
  public Arguments getDefaultParameters() {
    Arguments defaultParameters = new Arguments();
    defaultParameters.addArgument(PARAMETER_KAFKA_BROKERS, "${PARAMETER_KAFKA_BROKERS}");
    defaultParameters.addArgument(PARAMETER_KAFKA_TOPIC, "${PARAMETER_KAFKA_TOPIC}");
    defaultParameters.addArgument(PARAMETER_KAFKA_KEY, "${PARAMETER_KAFKA_KEY}");
    defaultParameters.addArgument(PARAMETER_KAFKA_MESSAGE, "${PARAMETER_KAFKA_MESSAGE}");
    defaultParameters.addArgument(PARAMETER_KAFKA_MESSAGE_SERIALIZER, "org.apache.kafka.common.serialization.StringSerializer");
    defaultParameters.addArgument(PARAMETER_KAFKA_KEY_SERIALIZER, "org.apache.kafka.common.serialization.StringSerializer");
    defaultParameters.addArgument(PARAMETER_KAFKA_SSL_KEYSTORE, "${PARAMETER_KAFKA_SSL_KEYSTORE}");
    defaultParameters.addArgument(PARAMETER_KAFKA_SSL_KEYSTORE_PASSWORD, "${PARAMETER_KAFKA_SSL_KEYSTORE_PASSWORD}");
    defaultParameters.addArgument(PARAMETER_KAFKA_SSL_TRUSTSTORE, "${PARAMETER_KAFKA_SSL_TRUSTSTORE}");
    defaultParameters.addArgument(PARAMETER_KAFKA_SSL_TRUSTSTORE_PASSWORD, "${PARAMETER_KAFKA_SSL_TRUSTSTORE_PASSWORD}");
    defaultParameters.addArgument(PARAMETER_KAFKA_USE_SSL, "${PARAMETER_KAFKA_USE_SSL}");
    defaultParameters.addArgument(PARAMETER_KAFKA_COMPRESSION_TYPE, null);
    defaultParameters.addArgument(PARAMETER_KAFKA_PARTITION, null);
    return defaultParameters;
  }

  @Override
  public SampleResult runTest(JavaSamplerContext context) {
    SampleResult result = newSampleResult();
    String topic = context.getParameter(PARAMETER_KAFKA_TOPIC);
    String key = context.getParameter(PARAMETER_KAFKA_KEY);
    String message = context.getParameter(PARAMETER_KAFKA_MESSAGE);
    sampleResultStart(result, message);

    final ProducerRecord<String, String> producerRecord;
    String partitionString = context.getParameter(PARAMETER_KAFKA_PARTITION);
    if (Strings.isNullOrEmpty(partitionString)) {
      producerRecord = new ProducerRecord<String, String>(topic, key, message);
    } else {
      final int partitionNumber = Integer.parseInt(partitionString);
      producerRecord = new ProducerRecord<String, String>(topic, partitionNumber, key, message);
    }

    try {
      producer.send(producerRecord);
      sampleResultSuccess(result, null);
    } catch (Exception e) {
      sampleResultFailed(result, "500", e);
    }
    return result;
  }

  /**
   * Use UTF-8 for encoding of strings
   */
  private static final String ENCODING = "UTF-8";

  /**
   * Factory for creating new {@link SampleResult}s.
   */
  private SampleResult newSampleResult() {
    SampleResult result = new SampleResult();
    result.setDataEncoding(ENCODING);
    result.setDataType(SampleResult.TEXT);
    return result;
  }

  /**
   * Start the sample request and set the {@code samplerData} to {@code data}.
   *
   * @param result
   *          the sample result to update
   * @param data
   *          the request to set as {@code samplerData}
   */
  private void sampleResultStart(SampleResult result, String data) {
    result.setSamplerData(data);
    result.sampleStart();
  }

  /**
   * Mark the sample result as {@code end}ed and {@code successful} with an "OK" {@code responseCode},
   * and if the response is not {@code null} then set the {@code responseData} to {@code response},
   * otherwise it is marked as not requiring a response.
   *
   * @param result sample result to change
   * @param response the successful result message, may be null.
   */
  private void sampleResultSuccess(SampleResult result, /* @Nullable */ String response) {
    result.sampleEnd();
    result.setSuccessful(true);
    result.setResponseCodeOK();
    if (response != null) {
      result.setResponseData(response, ENCODING);
    }
    else {
      result.setResponseData("No response required", ENCODING);
    }
  }

  /**
   * Mark the sample result as @{code end}ed and not {@code successful}, and set the
   * {@code responseCode} to {@code reason}.
   *
   * @param result the sample result to change
   * @param reason the failure reason
   */
  private void sampleResultFailed(SampleResult result, String reason) {
    result.sampleEnd();
    result.setSuccessful(false);
    result.setResponseCode(reason);
  }

  /**
   * Mark the sample result as @{code end}ed and not {@code successful}, set the
   * {@code responseCode} to {@code reason}, and set {@code responseData} to the stack trace.
   *
   * @param result the sample result to change
   * @param exception the failure exception
   */
  private void sampleResultFailed(SampleResult result, String reason, Exception exception) {
    sampleResultFailed(result, reason);
    result.setResponseMessage("Exception: " + exception);
    result.setResponseData(getStackTrace(exception), ENCODING);
  }

  /**
   * Return the stack trace as a string.
   *
   * @param exception the exception containing the stack trace
   * @return the stack trace
   */
  private String getStackTrace(Exception exception) {
    StringWriter stringWriter = new StringWriter();
    exception.printStackTrace(new PrintWriter(stringWriter));
    return stringWriter.toString();
  }
}