content: package io.github.delirius325.jmeter.backendlistener.elasticsearch;
  
  import java.net.InetAddress;
  import java.net.UnknownHostException;
  import java.text.ParseException;
  import java.text.SimpleDateFormat;
  import java.time.LocalDateTime;
  import java.time.format.DateTimeFormatter;
  import java.util.Calendar;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.LinkedList;
  import java.util.Map;
  import java.util.Set;
  
  import org.apache.jmeter.assertions.AssertionResult;
  import org.apache.jmeter.samplers.SampleResult;
  import org.apache.jmeter.threads.JMeterContextService;
  import org.apache.jmeter.visualizers.backend.BackendListenerContext;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static org.apache.commons.lang3.math.NumberUtils.isCreatable;
  
  public class ElasticSearchMetric {
      private static final Logger logger = LoggerFactory.getLogger(ElasticSearchMetric.class);
      private static final String HOSTNAME = solveHostName();
  
      private SampleResult sampleResult;
      private String esTestMode;
      private String esTimestamp;
      private int ciBuildNumber;
      private HashMap<String, Object> json;
      private Set<String> fields;
      private boolean allReqHeaders;
      private boolean allResHeaders;
  
      public ElasticSearchMetric(
              SampleResult sr, String testMode, String timeStamp, int buildNumber,
              boolean parseReqHeaders, boolean parseResHeaders, Set<String> fields) {
          this.sampleResult = sr;
          this.esTestMode = testMode.trim();
          this.esTimestamp = timeStamp.trim();
          this.ciBuildNumber = buildNumber;
          this.json = new HashMap<>();
          this.allReqHeaders = parseReqHeaders;
          this.allResHeaders = parseResHeaders;
          this.fields = fields;
      }
  
      private static String solveHostName() {
          try {
              return InetAddress.getLocalHost().getHostName();
          } catch (UnknownHostException e) {
              logger.warn("Could not resolve host name, falling back to localhost", e);
              return "localhost";
          }
      }
  
      /**
       * This method returns the current metric as a Map(String, Object) for the provided sampleResult
       *
       * @param context BackendListenerContext
       * @return a JSON Object as Map(String, Object)
       */
      public Map<String, Object> getMetric(BackendListenerContext context) throws Exception {
          SimpleDateFormat sdf = new SimpleDateFormat(this.esTimestamp);
  
          //add all the default SampleResult parameters
          addFilteredJSON("AllThreads", this.sampleResult.getAllThreads());
          addFilteredJSON("BodySize", this.sampleResult.getBodySizeAsLong());
          addFilteredJSON("Bytes", this.sampleResult.getBytesAsLong());
          addFilteredJSON("SentBytes", this.sampleResult.getSentBytes());
          addFilteredJSON("ConnectTime", this.sampleResult.getConnectTime());
          addFilteredJSON("ContentType", this.sampleResult.getContentType());
          addFilteredJSON("DataType", this.sampleResult.getDataType());
          addFilteredJSON("ErrorCount", this.sampleResult.getErrorCount());
          addFilteredJSON("GrpThreads", this.sampleResult.getGroupThreads());
          addFilteredJSON("IdleTime", this.sampleResult.getIdleTime());
          addFilteredJSON("Latency", this.sampleResult.getLatency());
          addFilteredJSON("ResponseTime", this.sampleResult.getTime());
          addFilteredJSON("SampleCount", this.sampleResult.getSampleCount());
          addFilteredJSON("SampleLabel", this.sampleResult.getSampleLabel());
          addFilteredJSON("ThreadName", this.sampleResult.getThreadName());
          addFilteredJSON("URL", this.sampleResult.getURL());
          addFilteredJSON("ResponseCode", this.sampleResult.getResponseCode());
          addFilteredJSON("TestStartTime", JMeterContextService.getTestStartTime());
          addFilteredJSON("SampleStartTime", sdf.format(new Date(this.sampleResult.getStartTime())));
          addFilteredJSON("SampleEndTime", sdf.format(new Date(this.sampleResult.getEndTime())));
          addFilteredJSON("Timestamp", this.sampleResult.getTimeStamp());
          addFilteredJSON("InjectorHostname", HOSTNAME);
  
          // Add the details according to the mode that is set
          switch (this.esTestMode) {
              case "debug":
                  addDetails();
                  break;
              case "error":
                  addDetails();
                  break;
              case "info":
                  if (!this.sampleResult.isSuccessful())
                      addDetails();
                  break;
              default:
                  break;
          }
  
          addAssertions();
          addElapsedTime();
          addElapsedDuration();
          addCustomFields(context);
          parseHeadersAsJsonProps(this.allReqHeaders, this.allResHeaders);
  
          return this.json;
      }
  
      /**
       * This method adds all the assertions for the current sampleResult
       */
      private void addAssertions() {
          AssertionResult[] assertionResults = this.sampleResult.getAssertionResults();
          if (assertionResults != null) {
              Map<String, Object>[] assertionArray = new HashMap[assertionResults.length];
              Integer i = 0;
              String failureMessage = "";
              boolean isFailure = false;
              for (AssertionResult assertionResult : assertionResults) {
                  HashMap<String, Object> assertionMap = new HashMap<>();
                  boolean failure = assertionResult.isFailure() || assertionResult.isError();
                  isFailure = isFailure || assertionResult.isFailure() || assertionResult.isError();
                  assertionMap.put("failure", failure);
                  assertionMap.put("failureMessage", assertionResult.getFailureMessage());
                  failureMessage += assertionResult.getFailureMessage() + "\n";
                  assertionMap.put("name", assertionResult.getName());
                  assertionArray[i] = assertionMap;
                  i++;
              }
              addFilteredJSON("AssertionResults", assertionArray);
              addFilteredJSON("FailureMessage", failureMessage);
              addFilteredJSON("Success", !isFailure);
          }
      }
  
      /**
       * This method adds the ElapsedTime as a key:value pair in the JSON object. Also, depending on whether or not the
       * tests were launched from a CI tool (i.e Jenkins), it will add a hard-coded version of the ElapsedTime for results
       * comparison purposes
       */
      private void addElapsedTime() {
          Date elapsedTime;
  
          if (this.ciBuildNumber != 0) {
              elapsedTime = getElapsedTime(true);
              addFilteredJSON("BuildNumber", this.ciBuildNumber);
  
              if (elapsedTime != null)
                  addFilteredJSON("ElapsedTimeComparison", elapsedTime.getTime());
          }
  
          elapsedTime = getElapsedTime(false);
          if (elapsedTime != null)
              addFilteredJSON("ElapsedTime", elapsedTime.getTime());
      }
  
      /**
       * This method adds the ElapsedDuration in seconds.
       */
      private void addElapsedDuration() {
          long elapsed = (System.currentTimeMillis() - JMeterContextService.getTestStartTime()) / 1000;
          addFilteredJSON("ElapsedDuration", elapsed);
      }
  
  
      /**
       * Methods that add all custom fields added by the user in the Backend Listener's GUI panel
       *
       * @param context BackendListenerContext
       */
      private void addCustomFields(BackendListenerContext context) {
          Iterator<String> pluginParameters = context.getParameterNamesIterator();
          String parameter;
          while (pluginParameters.hasNext()) {
              String parameterName = pluginParameters.next();
  
              if (!parameterName.startsWith("es.") && context.containsParameter(parameterName)
                      && !"".equals(parameter = context.getParameter(parameterName).trim())) {
                  if (isCreatable(parameter)) {
                      addFilteredJSON(parameterName, Long.parseLong(parameter));
                  } else {
                      addFilteredJSON(parameterName, parameter);
                  }
              }
          }
      }
  
      /**
       * Method that adds the request and response's body/headers
       */
      private void addDetails() {
          addFilteredJSON("RequestHeaders", this.sampleResult.getRequestHeaders());
          addFilteredJSON("RequestBody", this.sampleResult.getSamplerData());
          addFilteredJSON("ResponseHeaders", this.sampleResult.getResponseHeaders());
          addFilteredJSON("ResponseBody", this.sampleResult.getResponseDataAsString());
          addFilteredJSON("ResponseMessage", this.sampleResult.getResponseMessage());
      }
  
      /**
       * This method will parse the headers and look for custom variables passed through as header. It can also separate
       * all headers into different ElasticSearch document properties by passing "true" This is a work-around for the
       * native behaviour of JMeter, where variables are not accessible within the backend listener.
       *
       * @param allReqHeaders boolean to determine if the user wants to separate ALL request headers into different ES JSON
       *                      properties.
       * @param allResHeaders boolean to determine if the user wants to separate ALL response headers into different ES JSON
       *                      properties.
       *                      <p>
       *                      NOTE: This will be fixed as soon as a patch comes in for JMeter to change the behaviour.
       */
      private void parseHeadersAsJsonProps(boolean allReqHeaders, boolean allResHeaders) {
          LinkedList<String[]> headersArrayList = new LinkedList<String[]>();
  
          if (allReqHeaders) {
              headersArrayList.add(this.sampleResult.getRequestHeaders().split("\n"));
          }
  
          if (allResHeaders) {
              headersArrayList.add(this.sampleResult.getResponseHeaders().split("\n"));
          }
  
          if (!allReqHeaders && !allResHeaders) {
              headersArrayList.add(this.sampleResult.getRequestHeaders().split("\n"));
              headersArrayList.add(this.sampleResult.getResponseHeaders().split("\n"));
          }
  
          for (String[] lines : headersArrayList) {
              for (int i = 0; i < lines.length; i++) {
                  String[] header = lines[i].split(":", 2);
  
                  // if not all res/req headers and header contains special X-tag
                  if (!allReqHeaders && !allResHeaders && header.length > 1) {
                      if (header[0].startsWith("X-es-backend-")) {
                          this.json.put(header[0].replaceAll("X-es-backend-", "").trim(), header[1].trim());
                      }
                  }
  
                  if ((allReqHeaders || allResHeaders) && header.length > 1) {
                      this.json.put(header[0].trim(), header[1].trim());
                  }
              }
          }
      }
  
      /**
       * Adds a given key-value pair to JSON if the key is contained in the field filter or in case of empty field filter
       *
       * @param key
       * @param value
       */
      private void addFilteredJSON(String key, Object value) {
          if (this.fields.size() == 0 || this.fields.contains(key.toLowerCase())) {
              this.json.put(key, value);
          }
      }
  
      /**
       * This method is meant to return the elapsed time in a human readable format. The purpose of this is mostly for
       * build comparison in Kibana. By doing this, the user is able to set the X-axis of his graph to this date and split
       * the series by build numbers. It allows him to overlap test results and see if there is regression or not.
       *
       * @param forBuildComparison boolean to determine if there is CI (continuous integration) or not
       * @return The elapsed time in YYYY-MM-dd HH:mm:ss format
       */
      public Date getElapsedTime(boolean forBuildComparison) {
          String sElapsed;
          //Calculate the elapsed time (Starting from midnight on a random day - enables us to compare of two loads over their duration)
          long start = JMeterContextService.getTestStartTime();
          long end = System.currentTimeMillis();
          long elapsed = (end - start);
          long minutes = (elapsed / 1000) / 60;
          long seconds = (elapsed / 1000) % 60;
  
          Calendar cal = Calendar.getInstance();
          cal.set(Calendar.HOUR_OF_DAY, 0); //If there is more than an hour of data, the number of minutes/seconds will increment this
          cal.set(Calendar.MINUTE, (int) minutes);
          cal.set(Calendar.SECOND, (int) seconds);
  
          if (forBuildComparison) {
              sElapsed = String.format("2017-01-01 %02d:%02d:%02d", cal.get(Calendar.HOUR_OF_DAY),
                      cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND));
          } else {
              sElapsed = String.format("%s %02d:%02d:%02d",
                      DateTimeFormatter.ofPattern("yyyy-mm-dd").format(LocalDateTime.now()),
                      cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND));
          }
  
          SimpleDateFormat formatter = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
          try {
              return formatter.parse(sElapsed);
          } catch (ParseException e) {
              logger.error("Unexpected error occured computing elapsed date", e);
              return null;
          }
      }
  
  }
  