/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 *
 */

/*
 * Created on Sep 7, 2004
 */
package org.apache.jmeter.samplers;

import java.io.Serializable;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import org.apache.jmeter.save.CSVSaveService;
import org.apache.jmeter.testelement.TestPlan;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.util.JMeterError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/*
 * N.B. to add a new field, remember the following
 * - static _xyz
 * - instance xyz=_xyz
 * - clone s.xyz = xyz (perhaps)
 * - setXyz(boolean)
 * - saveXyz()
 * - add Xyz to SAVE_CONFIG_NAMES list
 * - update SampleSaveConfigurationConverter to add new fields to marshall() and shouldSerialiseMember()
 * - update ctor SampleSaveConfiguration(boolean value) to set the value if it is a boolean property
 * - update SampleResultConverter and/or HTTPSampleConverter
 * - update CSVSaveService: CSV_XXXX, makeResultFromDelimitedString, printableFieldNamesToString, static{}
 * - update messages.properties to add save_xyz entry
 * - update jmeter.properties to add new property
 * - update listeners.xml to add new property, CSV and XML names etc.
 * - take screenshot sample_result_config.png
 * - update listeners.xml and component_reference.xml with new dimensions (might not change)
 */

/**
 * Holds details of which sample attributes to save.
 *
 * The pop-up dialogue for this is created by the class SavePropertyDialog, which assumes:
 * <p>
 * For each field <em>XXX</em>
 * <ul>
 *  <li>methods have the signature "boolean save<em>XXX</em>()"</li>
 *  <li>a corresponding "void set<em>XXX</em>(boolean)" method</li>
 *  <li>messages.properties contains the key save_<em>XXX</em></li>
 * </ul>
 */
public class SampleSaveConfiguration implements Cloneable, Serializable {
    private static final long serialVersionUID = 8L;

    private static final Logger log = LoggerFactory.getLogger(SampleSaveConfiguration.class);

    // ---------------------------------------------------------------------
    // PROPERTY FILE CONSTANTS
    // ---------------------------------------------------------------------

    /** Indicates that the results file should be in XML format. * */
    private static final String XML = "xml"; // $NON_NLS-1$

    /** Indicates that the results file should be in CSV format. * */
    private static final String CSV = "csv"; // $NON_NLS-1$

    /** A properties file indicator for true. * */
    private static final String TRUE = "true"; // $NON_NLS-1$

    /** A properties file indicator for false. * */
    private static final String FALSE = "false"; // $NON_NLS-1$

    /** A properties file indicator for milliseconds. * */
    public static final String MILLISECONDS = "ms"; // $NON_NLS-1$

    /** A properties file indicator for none. * */
    public static final String NONE = "none"; // $NON_NLS-1$

    /** A properties file indicator for the first of a series. * */
    private static final String FIRST = "first"; // $NON_NLS-1$

    /** A properties file indicator for all of a series. * */
    private static final String ALL = "all"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating which assertion results should be
     * saved.
     **************************************************************************/
    public static final String ASSERTION_RESULTS_FAILURE_MESSAGE_PROP =
        "jmeter.save.saveservice.assertion_results_failure_message";  // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating which assertion results should be
     * saved.
     **************************************************************************/
    private static final String ASSERTION_RESULTS_PROP = "jmeter.save.saveservice.assertion_results"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating which delimiter should be used when
     * saving in a delimited values format.
     **************************************************************************/
    public static final String DEFAULT_DELIMITER_PROP = "jmeter.save.saveservice.default_delimiter"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating which format should be used when
     * saving the results, e.g., xml or csv.
     **************************************************************************/
    private static final String OUTPUT_FORMAT_PROP = "jmeter.save.saveservice.output_format"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating whether field names should be printed
     * to a delimited file.
     **************************************************************************/
    private static final String PRINT_FIELD_NAMES_PROP = "jmeter.save.saveservice.print_field_names"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating whether the data type should be
     * saved.
     **************************************************************************/
    private static final String SAVE_DATA_TYPE_PROP = "jmeter.save.saveservice.data_type"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating whether the label should be saved.
     **************************************************************************/
    private static final String SAVE_LABEL_PROP = "jmeter.save.saveservice.label"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating whether the response code should be
     * saved.
     **************************************************************************/
    private static final String SAVE_RESPONSE_CODE_PROP = "jmeter.save.saveservice.response_code"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating whether the response data should be
     * saved.
     **************************************************************************/
    private static final String SAVE_RESPONSE_DATA_PROP = "jmeter.save.saveservice.response_data"; // $NON_NLS-1$

    private static final String SAVE_RESPONSE_DATA_ON_ERROR_PROP = "jmeter.save.saveservice.response_data.on_error"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating whether the response message should
     * be saved.
     **************************************************************************/
    private static final String SAVE_RESPONSE_MESSAGE_PROP = "jmeter.save.saveservice.response_message"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating whether the success indicator should
     * be saved.
     **************************************************************************/
    private static final String SAVE_SUCCESSFUL_PROP = "jmeter.save.saveservice.successful"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating whether the thread name should be
     * saved.
     **************************************************************************/
    private static final String SAVE_THREAD_NAME_PROP = "jmeter.save.saveservice.thread_name"; // $NON_NLS-1$

    // Save bytes read
    private static final String SAVE_BYTES_PROP = "jmeter.save.saveservice.bytes"; // $NON_NLS-1$
    
    // Save bytes written
    private static final String SAVE_SENT_BYTES_PROP = "jmeter.save.saveservice.sent_bytes"; // $NON_NLS-1$

    // Save URL
    private static final String SAVE_URL_PROP = "jmeter.save.saveservice.url"; // $NON_NLS-1$

    // Save fileName for ResultSaver
    private static final String SAVE_FILENAME_PROP = "jmeter.save.saveservice.filename"; // $NON_NLS-1$

    // Save hostname for ResultSaver
    private static final String SAVE_HOSTNAME_PROP = "jmeter.save.saveservice.hostname"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property indicating whether the time should be saved.
     **************************************************************************/
    private static final String SAVE_TIME_PROP = "jmeter.save.saveservice.time"; // $NON_NLS-1$

    /***************************************************************************
     * The name of the property giving the format of the time stamp
     **************************************************************************/
    private static final String TIME_STAMP_FORMAT_PROP = "jmeter.save.saveservice.timestamp_format"; // $NON_NLS-1$

    private static final String SUBRESULTS_PROP      = "jmeter.save.saveservice.subresults"; // $NON_NLS-1$
    private static final String ASSERTIONS_PROP      = "jmeter.save.saveservice.assertions"; // $NON_NLS-1$
    private static final String LATENCY_PROP         = "jmeter.save.saveservice.latency"; // $NON_NLS-1$
    private static final String CONNECT_TIME_PROP    = "jmeter.save.saveservice.connect_time"; // $NON_NLS-1$
    private static final String SAMPLERDATA_PROP     = "jmeter.save.saveservice.samplerData"; // $NON_NLS-1$
    private static final String RESPONSEHEADERS_PROP = "jmeter.save.saveservice.responseHeaders"; // $NON_NLS-1$
    private static final String REQUESTHEADERS_PROP  = "jmeter.save.saveservice.requestHeaders"; // $NON_NLS-1$
    private static final String ENCODING_PROP        = "jmeter.save.saveservice.encoding"; // $NON_NLS-1$


    // optional processing instruction for line 2; e.g.
    // <?xml-stylesheet type="text/xsl" href="../extras/jmeter-results-detail-report_21.xsl"?>
    private static final String XML_PI               = "jmeter.save.saveservice.xml_pi"; // $NON_NLS-1$

    private static final String SAVE_THREAD_COUNTS   = "jmeter.save.saveservice.thread_counts"; // $NON_NLS-1$

    private static final String SAVE_SAMPLE_COUNT    = "jmeter.save.saveservice.sample_count"; // $NON_NLS-1$

    private static final String SAVE_IDLE_TIME       = "jmeter.save.saveservice.idle_time"; // $NON_NLS-1$
    
    // Defaults from properties:
    private static final boolean TIME;
    private static final boolean TIMESTAMP;
    private static final boolean SUCCESS;
    private static final boolean LABEL;
    private static final boolean CODE;
    private static final boolean MESSAGE;
    private static final boolean THREAD_NAME;
    private static final boolean IS_XML;
    private static final boolean RESPONSE_DATA;
    private static final boolean DATATYPE;
    private static final boolean ENCODING;
    private static final boolean ASSERTIONS;
    private static final boolean LATENCY;
    private static final boolean CONNECT_TIME;
    private static final boolean SUB_RESULTS;
    private static final boolean SAMPLER_DATA;
    private static final boolean FIELD_NAMES;
    private static final boolean RESPONSE_HEADERS;
    private static final boolean REQUEST_HEADERS;

    private static final boolean RESPONSE_DATA_ON_ERROR;

    private static final boolean SAVE_ASSERTION_RESULTS_FAILURE_MESSAGE;

    private static final int ASSERTIONS_RESULT_TO_SAVE;

    // TODO turn into method?
    public static final int SAVE_NO_ASSERTIONS = 0;

    public static final int SAVE_FIRST_ASSERTION = SAVE_NO_ASSERTIONS + 1;

    public static final int SAVE_ALL_ASSERTIONS = SAVE_FIRST_ASSERTION + 1;

    private static final boolean PRINT_MILLISECONDS;

    private static final boolean BYTES;
    
    private static final boolean SENT_BYTES;

    private static final boolean URL;

    private static final boolean FILE_NAME;

    private static final boolean HOST_NAME;

    private static final boolean THREAD_COUNTS;

    private static final boolean SAMPLE_COUNT;

    private static final String DATE_FORMAT;

    /**
     * The string used to separate fields when stored to disk, for example, the
     * comma for CSV files.
     */
    private static final String DELIMITER;

    private static final boolean IDLE_TIME;

    public static final String DEFAULT_DELIMITER = ","; // $NON_NLS-1$

    // Read in the properties having to do with saving from a properties file.
    static {
        Properties props = JMeterUtils.getJMeterProperties();

        SUB_RESULTS      = TRUE.equalsIgnoreCase(props.getProperty(SUBRESULTS_PROP, TRUE));
        ASSERTIONS      = TRUE.equalsIgnoreCase(props.getProperty(ASSERTIONS_PROP, TRUE));
        LATENCY         = TRUE.equalsIgnoreCase(props.getProperty(LATENCY_PROP, TRUE));
        CONNECT_TIME     = TRUE.equalsIgnoreCase(props.getProperty(CONNECT_TIME_PROP, TRUE));
        SAMPLER_DATA     = TRUE.equalsIgnoreCase(props.getProperty(SAMPLERDATA_PROP, FALSE));
        RESPONSE_HEADERS = TRUE.equalsIgnoreCase(props.getProperty(RESPONSEHEADERS_PROP, FALSE));
        REQUEST_HEADERS  = TRUE.equalsIgnoreCase(props.getProperty(REQUESTHEADERS_PROP, FALSE));
        ENCODING        = TRUE.equalsIgnoreCase(props.getProperty(ENCODING_PROP, FALSE));

        String dlm = JMeterUtils.getDelimiter(props.getProperty(DEFAULT_DELIMITER_PROP, DEFAULT_DELIMITER));
        char ch = dlm.charAt(0);

        if (CharUtils.isAsciiAlphanumeric(ch) || ch == CSVSaveService.QUOTING_CHAR){
            throw new JMeterError("Delimiter '"+ch+"' must not be alphanumeric or "+CSVSaveService.QUOTING_CHAR+".");
        }

        if (ch != '\t' && !CharUtils.isAsciiPrintable(ch)){
            throw new JMeterError("Delimiter (code "+(int)ch+") must be printable.");
        }

        DELIMITER = dlm;

        FIELD_NAMES = TRUE.equalsIgnoreCase(props.getProperty(PRINT_FIELD_NAMES_PROP, TRUE));

        DATATYPE = TRUE.equalsIgnoreCase(props.getProperty(SAVE_DATA_TYPE_PROP, TRUE));

        LABEL = TRUE.equalsIgnoreCase(props.getProperty(SAVE_LABEL_PROP, TRUE));

        CODE = TRUE.equalsIgnoreCase(props.getProperty(SAVE_RESPONSE_CODE_PROP, TRUE));

        RESPONSE_DATA = TRUE.equalsIgnoreCase(props.getProperty(SAVE_RESPONSE_DATA_PROP, FALSE));

        RESPONSE_DATA_ON_ERROR = TRUE.equalsIgnoreCase(props.getProperty(SAVE_RESPONSE_DATA_ON_ERROR_PROP, FALSE));

        MESSAGE = TRUE.equalsIgnoreCase(props.getProperty(SAVE_RESPONSE_MESSAGE_PROP, TRUE));

        SUCCESS = TRUE.equalsIgnoreCase(props.getProperty(SAVE_SUCCESSFUL_PROP, TRUE));

        THREAD_NAME = TRUE.equalsIgnoreCase(props.getProperty(SAVE_THREAD_NAME_PROP, TRUE));

        BYTES = TRUE.equalsIgnoreCase(props.getProperty(SAVE_BYTES_PROP, TRUE));
        
        SENT_BYTES = TRUE.equalsIgnoreCase(props.getProperty(SAVE_SENT_BYTES_PROP, TRUE));

        URL = TRUE.equalsIgnoreCase(props.getProperty(SAVE_URL_PROP, TRUE));

        FILE_NAME = TRUE.equalsIgnoreCase(props.getProperty(SAVE_FILENAME_PROP, FALSE));

        HOST_NAME = TRUE.equalsIgnoreCase(props.getProperty(SAVE_HOSTNAME_PROP, FALSE));

        TIME = TRUE.equalsIgnoreCase(props.getProperty(SAVE_TIME_PROP, TRUE));

        String temporaryTimestampFormat = props.getProperty(TIME_STAMP_FORMAT_PROP, MILLISECONDS);

        PRINT_MILLISECONDS = MILLISECONDS.equalsIgnoreCase(temporaryTimestampFormat);

        if (!PRINT_MILLISECONDS && !NONE.equalsIgnoreCase(temporaryTimestampFormat)) {
            DATE_FORMAT = validateFormat(temporaryTimestampFormat);
        } else {
            DATE_FORMAT = null;
        }

        TIMESTAMP = !NONE.equalsIgnoreCase(temporaryTimestampFormat);// reversed compare allows for null

        SAVE_ASSERTION_RESULTS_FAILURE_MESSAGE = TRUE.equalsIgnoreCase(props.getProperty(
                ASSERTION_RESULTS_FAILURE_MESSAGE_PROP, TRUE));

        String whichAssertionResults = props.getProperty(ASSERTION_RESULTS_PROP, NONE);
        if (NONE.equals(whichAssertionResults)) {
            ASSERTIONS_RESULT_TO_SAVE = SAVE_NO_ASSERTIONS;
        } else if (FIRST.equals(whichAssertionResults)) {
            ASSERTIONS_RESULT_TO_SAVE = SAVE_FIRST_ASSERTION;
        } else if (ALL.equals(whichAssertionResults)) {
            ASSERTIONS_RESULT_TO_SAVE = SAVE_ALL_ASSERTIONS;
        } else {
            ASSERTIONS_RESULT_TO_SAVE = 0;
        }

        String howToSave = props.getProperty(OUTPUT_FORMAT_PROP, CSV);

        if (XML.equals(howToSave)) {
            IS_XML = true;
        } else {
            if (!CSV.equals(howToSave)) {
                log.warn("{} has unexepected value: '{}' - assuming 'csv' format", OUTPUT_FORMAT_PROP, howToSave);
            }
            IS_XML = false;
        }

        THREAD_COUNTS=TRUE.equalsIgnoreCase(props.getProperty(SAVE_THREAD_COUNTS, TRUE));

        SAMPLE_COUNT=TRUE.equalsIgnoreCase(props.getProperty(SAVE_SAMPLE_COUNT, FALSE));

        IDLE_TIME=TRUE.equalsIgnoreCase(props.getProperty(SAVE_IDLE_TIME, TRUE));
    }

    private static final SampleSaveConfiguration STATIC_SAVE_CONFIGURATION = new SampleSaveConfiguration();

    // for test code only
    static final String CONFIG_GETTER_PREFIX = "save";  // $NON-NLS-1$
    
    // for test code only
    static final String CONFIG_SETTER_PREFIX = "set";  // $NON-NLS-1$

    /**
     * List of saveXXX/setXXX(boolean) methods which is used to build the Sample Result Save Configuration dialog.
     * New method names should be added at the end so that existing layouts are not affected.
     */
    // The current order is derived from http://jmeter.apache.org/usermanual/listeners.html#csvlogformat
    // TODO this may not be the ideal order; fix further and update the screenshot(s)
    public static final List<String> SAVE_CONFIG_NAMES = Collections.unmodifiableList(Arrays.asList(new String[]{
        "AsXml",
        "FieldNames", // CSV
        "Timestamp",
        "Time", // elapsed
        "Label",
        "Code", // Response Code
        "Message", // Response Message
        "ThreadName",
        "DataType",
        "Success",
        "AssertionResultsFailureMessage",
        "Bytes",
        "SentBytes",
        "ThreadCounts", // grpThreads and allThreads
        "Url",
        "FileName",
        "Latency",
        "ConnectTime",
        "Encoding",
        "SampleCount", // Sample and Error Count
        "Hostname",
        "IdleTime",
        "RequestHeaders", // XML
        "SamplerData", // XML
        "ResponseHeaders", // XML
        "ResponseData", // XML
        "Subresults", // XML
        "Assertions", // XML
    }));
    // N.B. Remember to update the equals and hashCode methods when adding new variables.

    // Initialise values from properties
    private boolean time = TIME;
    private boolean latency = LATENCY;
    private boolean connectTime=CONNECT_TIME;
    private boolean timestamp = TIMESTAMP;
    private boolean success = SUCCESS;
    private boolean label = LABEL;
    private boolean code = CODE;
    private boolean message = MESSAGE;
    private boolean threadName = THREAD_NAME;
    private boolean dataType = DATATYPE;
    private boolean encoding = ENCODING;
    private boolean assertions = ASSERTIONS;
    private boolean subresults = SUB_RESULTS;
    private boolean responseData = RESPONSE_DATA;
    private boolean samplerData = SAMPLER_DATA;
    private boolean xml = IS_XML;
    private boolean fieldNames = FIELD_NAMES;
    private boolean responseHeaders = RESPONSE_HEADERS;
    private boolean requestHeaders = REQUEST_HEADERS;
    private boolean responseDataOnError = RESPONSE_DATA_ON_ERROR;

    private boolean saveAssertionResultsFailureMessage = SAVE_ASSERTION_RESULTS_FAILURE_MESSAGE;

    private boolean url = URL;
    private boolean bytes = BYTES;
    private boolean sentBytes = SENT_BYTES;
    private boolean fileName = FILE_NAME;

    private boolean hostname = HOST_NAME;

    private boolean threadCounts = THREAD_COUNTS;

    private boolean sampleCount = SAMPLE_COUNT;

    private boolean idleTime = IDLE_TIME;

    // Does not appear to be used (yet)
    private int assertionsResultsToSave = ASSERTIONS_RESULT_TO_SAVE;

    // Don't save this, as it is derived from the time format
    private boolean printMilliseconds = PRINT_MILLISECONDS;

    private transient String dateFormat = DATE_FORMAT;

    /** A formatter for the time stamp. 
     * Make transient as we don't want to save the FastDateFormat class
     * Also, there's currently no way to change the value via the GUI, so changing it
     * later means editing the JMX, or recreating the Listener.
     */
    private transient FastDateFormat timestampFormatter =
        dateFormat != null ? FastDateFormat.getInstance(dateFormat) : null;
    
    // Don't save this, as not settable via GUI
    private String delimiter = DELIMITER;

    // Don't save this - only needed for processing CSV headers currently
    private transient int varCount = 0;

    public SampleSaveConfiguration() {
    }

    /**
     * Alternate constructor for use by CsvSaveService
     *
     * @param value initial setting for boolean fields used in Config dialogue
     */
    public SampleSaveConfiguration(boolean value) {
        assertions = value;
        bytes = value;
        code = value;
        connectTime = value;
        dataType = value;
        encoding = value;
        fieldNames = value;
        fileName = value;
        hostname = value;
        idleTime = value;
        label = value;
        latency = value;
        message = value;
        printMilliseconds = PRINT_MILLISECONDS;//is derived from properties only
        requestHeaders = value;
        responseData = value;
        responseDataOnError = value;
        responseHeaders = value;
        sampleCount = value;
        samplerData = value;
        saveAssertionResultsFailureMessage = value;
        sentBytes = value;
        subresults = value;
        success = value;
        threadCounts = value;
        threadName = value;
        time = value;
        timestamp = value;
        url = value;
        xml = value;
    }
    
    public int getVarCount() { // Only for use by CSVSaveService
        return varCount;
    }

    public void setVarCount(int varCount) { // Only for use by CSVSaveService
        this.varCount = varCount;
    }

    // Give access to initial configuration
    public static SampleSaveConfiguration staticConfig() {
        return STATIC_SAVE_CONFIGURATION;
    }
    
    /**
     * Convert a config name to the method name of the getter.
     * The getter method returns a boolean.
     * @param configName the config name
     * @return the getter method name
     */
    public static final String getterName(String configName) {
        return CONFIG_GETTER_PREFIX + configName;
    }

    /**
     * Convert a config name to the method name of the setter
     * The setter method requires a boolean parameter.
     * @param configName the config name
     * @return the setter method name
     */
    public static final String setterName(String configName) {
        return CONFIG_SETTER_PREFIX + configName;
    }

    /**
     * Validate pattern
     * @param temporaryTimestampFormat DateFormat pattern
     * @return format if ok or null
     */
    private static String validateFormat(String temporaryTimestampFormat) {
        try {
            new SimpleDateFormat(temporaryTimestampFormat);
            if(log.isDebugEnabled()) {
                log.debug("Successfully validated pattern value {} for property {}",
                        temporaryTimestampFormat, TIME_STAMP_FORMAT_PROP);
            }
            return temporaryTimestampFormat;
        } catch(IllegalArgumentException ex) {
            log.error("Invalid pattern value {} for property {}", temporaryTimestampFormat, TIME_STAMP_FORMAT_PROP,
                    ex);
            return null;
        }
    }


    private Object readResolve(){
        setupDateFormat(DATE_FORMAT);
        return this;
    }

    /**
     * Initialize threadSafeLenientFormatter
     * @param pDateFormat String date format
     */
    private void setupDateFormat(String pDateFormat) {
        this.dateFormat = pDateFormat;
        if(dateFormat != null) {
            this.timestampFormatter = FastDateFormat.getInstance(dateFormat);
        } else {
            this.timestampFormatter = null;
        }
    }

    @Override
    public Object clone() {
        try {
            SampleSaveConfiguration clone = (SampleSaveConfiguration)super.clone();
            if(this.dateFormat != null) {
                clone.timestampFormatter = (FastDateFormat)this.threadSafeLenientFormatter().clone();
            }
            return clone;
        }
        catch(CloneNotSupportedException e) {
            throw new RuntimeException("Should not happen",e);
        }
    }

    @Override
    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }
        if((obj == null) || (obj.getClass() != this.getClass())) {
            return false;
        }
        // We know we are comparing to another SampleSaveConfiguration
        SampleSaveConfiguration s = (SampleSaveConfiguration)obj;
        boolean primitiveValues = s.time == time &&
            s.latency == latency &&
            s.connectTime == connectTime &&
            s.timestamp == timestamp &&
            s.success == success &&
            s.label == label &&
            s.code == code &&
            s.message == message &&
            s.threadName == threadName &&
            s.dataType == dataType &&
            s.encoding == encoding &&
            s.assertions == assertions &&
            s.subresults == subresults &&
            s.responseData == responseData &&
            s.samplerData == samplerData &&
            s.xml == xml &&
            s.fieldNames == fieldNames &&
            s.responseHeaders == responseHeaders &&
            s.requestHeaders == requestHeaders &&
            s.assertionsResultsToSave == assertionsResultsToSave &&
            s.saveAssertionResultsFailureMessage == saveAssertionResultsFailureMessage &&
            s.printMilliseconds == printMilliseconds &&
            s.responseDataOnError == responseDataOnError &&
            s.url == url &&
            s.bytes == bytes &&
            s.sentBytes == sentBytes &&
            s.fileName == fileName &&
            s.hostname == hostname &&
            s.sampleCount == sampleCount &&
            s.idleTime == idleTime &&
            s.threadCounts == threadCounts;

        boolean stringValues = false;
        if(primitiveValues) {
            stringValues = Objects.equals(delimiter, s.delimiter);
        }
        boolean complexValues = false;
        if(primitiveValues && stringValues) {
            complexValues = Objects.equals(dateFormat, s.dateFormat);
        }

        return primitiveValues && stringValues && complexValues;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 31 * hash + (time ? 1 : 0);
        hash = 31 * hash + (latency ? 1 : 0);
        hash = 31 * hash + (connectTime ? 1 : 0);
        hash = 31 * hash + (timestamp ? 1 : 0);
        hash = 31 * hash + (success ? 1 : 0);
        hash = 31 * hash + (label ? 1 : 0);
        hash = 31 * hash + (code ? 1 : 0);
        hash = 31 * hash + (message ? 1 : 0);
        hash = 31 * hash + (threadName ? 1 : 0);
        hash = 31 * hash + (dataType ? 1 : 0);
        hash = 31 * hash + (encoding ? 1 : 0);
        hash = 31 * hash + (assertions ? 1 : 0);
        hash = 31 * hash + (subresults ? 1 : 0);
        hash = 31 * hash + (responseData ? 1 : 0);
        hash = 31 * hash + (samplerData ? 1 : 0);
        hash = 31 * hash + (xml ? 1 : 0);
        hash = 31 * hash + (fieldNames ? 1 : 0);
        hash = 31 * hash + (responseHeaders ? 1 : 0);
        hash = 31 * hash + (requestHeaders ? 1 : 0);
        hash = 31 * hash + assertionsResultsToSave;
        hash = 31 * hash + (saveAssertionResultsFailureMessage ? 1 : 0);
        hash = 31 * hash + (printMilliseconds ? 1 : 0);
        hash = 31 * hash + (responseDataOnError ? 1 : 0);
        hash = 31 * hash + (url ? 1 : 0);
        hash = 31 * hash + (bytes ? 1 : 0);
        hash = 31 * hash + (sentBytes ? 1 : 0);
        hash = 31 * hash + (fileName ? 1 : 0);
        hash = 31 * hash + (hostname ? 1 : 0);
        hash = 31 * hash + (threadCounts ? 1 : 0);
        hash = 31 * hash + (delimiter != null  ? delimiter.hashCode() : 0);
        hash = 31 * hash + (dateFormat != null  ? dateFormat.hashCode() : 0);
        hash = 31 * hash + (sampleCount ? 1 : 0);
        hash = 31 * hash + (idleTime ? 1 : 0);

        return hash;
    }

    ///////////////////// Start of standard save/set access methods /////////////////////

    public boolean saveResponseHeaders() {
        return responseHeaders;
    }

    public void setResponseHeaders(boolean r) {
        responseHeaders = r;
    }

    public boolean saveRequestHeaders() {
        return requestHeaders;
    }

    public void setRequestHeaders(boolean r) {
        requestHeaders = r;
    }

    public boolean saveAssertions() {
        return assertions;
    }

    public void setAssertions(boolean assertions) {
        this.assertions = assertions;
    }

    public boolean saveCode() {
        return code;
    }

    public void setCode(boolean code) {
        this.code = code;
    }

    public boolean saveDataType() {
        return dataType;
    }

    public void setDataType(boolean dataType) {
        this.dataType = dataType;
    }

    public boolean saveEncoding() {
        return encoding;
    }

    public void setEncoding(boolean encoding) {
        this.encoding = encoding;
    }

    public boolean saveLabel() {
        return label;
    }

    public void setLabel(boolean label) {
        this.label = label;
    }

    public boolean saveLatency() {
        return latency;
    }

    public void setLatency(boolean latency) {
        this.latency = latency;
    }

    public boolean saveConnectTime() {
        return connectTime;
    }

    public void setConnectTime(boolean connectTime) {
        this.connectTime = connectTime;
    }

    public boolean saveMessage() {
        return message;
    }

    public void setMessage(boolean message) {
        this.message = message;
    }

    public boolean saveResponseData(SampleResult res) {
        return responseData || TestPlan.getFunctionalMode() || (responseDataOnError && !res.isSuccessful());
    }

    public boolean saveResponseData()
    {
        return responseData;
    }

    public void setResponseData(boolean responseData) {
        this.responseData = responseData;
    }

    public boolean saveSamplerData(SampleResult res) {
        return samplerData || TestPlan.getFunctionalMode() // as per 2.0 branch
                || (responseDataOnError && !res.isSuccessful());
    }

    public boolean saveSamplerData()
    {
        return samplerData;
    }

    public void setSamplerData(boolean samplerData) {
        this.samplerData = samplerData;
    }

    public boolean saveSubresults() {
        return subresults;
    }

    public void setSubresults(boolean subresults) {
        this.subresults = subresults;
    }

    public boolean saveSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public boolean saveThreadName() {
        return threadName;
    }

    public void setThreadName(boolean threadName) {
        this.threadName = threadName;
    }

    public boolean saveTime() {
        return time;
    }

    public void setTime(boolean time) {
        this.time = time;
    }

    public boolean saveTimestamp() {
        return timestamp;
    }

    public void setTimestamp(boolean timestamp) {
        this.timestamp = timestamp;
    }

    public boolean saveAsXml() {
        return xml;
    }

    public void setAsXml(boolean xml) {
        this.xml = xml;
    }

    public boolean saveFieldNames() {
        return fieldNames;
    }

    public void setFieldNames(boolean printFieldNames) {
        this.fieldNames = printFieldNames;
    }

    public boolean saveUrl() {
        return url;
    }

    public void setUrl(boolean save) {
        this.url = save;
    }

    public boolean saveBytes() {
        return bytes;
    }

    public void setBytes(boolean save) {
        this.bytes = save;
    }
    
    public boolean saveSentBytes() {
        return sentBytes;
    }

    public void setSentBytes(boolean save) {
        this.sentBytes = save;
    }

    public boolean saveFileName() {
        return fileName;
    }

    public void setFileName(boolean save) {
        this.fileName = save;
    }

    public boolean saveAssertionResultsFailureMessage() {
        return saveAssertionResultsFailureMessage;
    }

    public void setAssertionResultsFailureMessage(boolean b) {
        saveAssertionResultsFailureMessage = b;
    }

    public boolean saveThreadCounts() {
        return threadCounts;
    }

    public void setThreadCounts(boolean save) {
        this.threadCounts = save;
    }

    public boolean saveSampleCount() {
        return sampleCount;
    }

    public void setSampleCount(boolean save) {
        this.sampleCount = save;
    }

    ///////////////// End of standard field accessors /////////////////////

    
    /**
     * Intended for use by CsvSaveService (and test cases)
     * @param fmt
     *            format of the date to be saved. If <code>null</code>
     *            milliseconds since epoch will be printed
     */
    public void setDateFormat(String fmt){
        printMilliseconds = fmt == null; // maintain relationship
        setupDateFormat(fmt);
    }

    public boolean printMilliseconds() {
        return printMilliseconds;
    }

    /**
     * @return {@link DateFormat} non lenient
     */
    public DateFormat strictDateFormatter() {
        if(dateFormat != null) {
            return new SimpleDateFormat(dateFormat);
        } else {
            return null;
        }
    }
    
    /**
     * @return {@link FastDateFormat} Thread safe lenient formatter
     */
    public FastDateFormat threadSafeLenientFormatter() {
        // When restored by XStream threadSafeLenientFormatter may not have 
        // been initialized
        if(timestampFormatter == null) {
            timestampFormatter = 
                    dateFormat != null ? FastDateFormat.getInstance(dateFormat) : null;
        }
        return timestampFormatter;
    }

    public int assertionsResultsToSave() {
        return assertionsResultsToSave;
    }

    public String getDelimiter() {
        return delimiter;
    }

    public String getXmlPi() {
        return JMeterUtils.getJMeterProperties().getProperty(XML_PI, ""); // Defaults to empty;
    }

    // Used by old Save service
    public void setDelimiter(String delim) {
        delimiter=delim;
    }

    // Used by SampleSaveConfigurationConverter.unmarshall()
    public void setDefaultDelimiter() {
        delimiter=DELIMITER;
    }

    // Used by SampleSaveConfigurationConverter.unmarshall()
    public void setDefaultTimeStampFormat() {
        printMilliseconds=PRINT_MILLISECONDS;
        setupDateFormat(DATE_FORMAT);
    }

    public boolean saveHostname(){
        return hostname;
    }

    public void setHostname(boolean save){
        hostname = save;
    }

    public boolean saveIdleTime() {
        return idleTime;
    }

    public void setIdleTime(boolean save) {
        idleTime = save;
    }
}
