/*
 * 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.
 * 
 */

package org.apache.jmeter.save.converters;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;

import org.apache.commons.io.IOUtils;
import org.apache.jmeter.assertions.AssertionResult;
import org.apache.jmeter.samplers.SampleEvent;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.samplers.SampleSaveConfiguration;
import org.apache.jmeter.save.SaveService;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.jorphan.util.Converter;
import org.apache.log.Logger;

import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

/**
 * XStream Converter for the SampleResult class
 */
public class SampleResultConverter extends AbstractCollectionConverter {
	private static final Logger log = LoggingManager.getLoggerForClass();
 
    private static final String JAVA_LANG_STRING = "java.lang.String"; //$NON-NLS-1$
    private static final String ATT_CLASS = "class"; //$NON-NLS-1$

    // Element tags. Must be unique. Keep sorted.
    protected static final String TAG_COOKIES           = "cookies";          //$NON-NLS-1$
    protected static final String TAG_METHOD            = "method";           //$NON-NLS-1$
    protected static final String TAG_QUERY_STRING      = "queryString";      //$NON-NLS-1$
    protected static final String TAG_REDIRECT_LOCATION = "redirectLocation"; //$NON-NLS-1$
    protected static final String TAG_REQUEST_HEADER    = "requestHeader";    //$NON-NLS-1$

	//NOT USED protected   static final String TAG_URL               = "requestUrl";       //$NON-NLS-1$

    protected static final String TAG_RESPONSE_DATA     = "responseData";     //$NON-NLS-1$
    protected static final String TAG_RESPONSE_HEADER   = "responseHeader";   //$NON-NLS-1$
    protected static final String TAG_SAMPLER_DATA      = "samplerData";      //$NON-NLS-1$
    protected static final String TAG_RESPONSE_FILE     = "responseFile";     //$NON-NLS-1$

    // samplerData attributes. Must be unique. Keep sorted by string value.
    // Ensure the Listener documentation is updated when new attributes are added
    private static final String ATT_BYTES             = "by"; //$NON-NLS-1$
    private static final String ATT_DATA_ENCODING     = "de"; //$NON-NLS-1$
    private static final String ATT_DATA_TYPE         = "dt"; //$NON-NLS-1$
    private static final String ATT_ERROR_COUNT       = "ec"; //$NON-NLS-1$
    private static final String ATT_HOSTNAME          = "hn"; //$NON-NLS-1$
    private static final String ATT_LABEL             = "lb"; //$NON-NLS-1$
    private static final String ATT_LATENCY           = "lt"; //$NON-NLS-1$

    private static final String ATT_ALL_THRDS         = "na"; //$NON-NLS-1$
    private static final String ATT_GRP_THRDS         = "ng"; //$NON-NLS-1$

    // N.B. Originally the response code was saved with the code "rs" 
    // but retrieved with the code "rc". Changed to always use "rc", but
    // allow for "rs" when restoring values.
    private static final String ATT_RESPONSE_CODE     = "rc"; //$NON-NLS-1$
    private static final String ATT_RESPONSE_MESSAGE  = "rm"; //$NON-NLS-1$
    private static final String ATT_RESPONSE_CODE_OLD = "rs"; //$NON-NLS-1$
    
    private static final String ATT_SUCCESS           = "s";  //$NON-NLS-1$
    private static final String ATT_SAMPLE_COUNT      = "sc"; //$NON-NLS-1$
    private static final String ATT_TIME              = "t";  //$NON-NLS-1$
    private static final String ATT_THREADNAME        = "tn"; //$NON-NLS-1$
    private static final String ATT_TIME_STAMP        = "ts"; //$NON-NLS-1$

	/**
	 * Returns the converter version; used to check for possible
	 * incompatibilities
	 */
	public static String getVersion() {
		return "$Revision: 646610 $"; //$NON-NLS-1$
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.thoughtworks.xstream.converters.Converter#canConvert(java.lang.Class)
	 */
	public boolean canConvert(Class arg0) {
		return SampleResult.class.equals(arg0);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.thoughtworks.xstream.converters.Converter#marshal(java.lang.Object,
	 *      com.thoughtworks.xstream.io.HierarchicalStreamWriter,
	 *      com.thoughtworks.xstream.converters.MarshallingContext)
	 */
	public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context) {
		SampleResult res = (SampleResult) obj;
		SampleSaveConfiguration save = res.getSaveConfig();
		setAttributes(writer, context, res, save);
		saveAssertions(writer, context, res, save);
		saveSubResults(writer, context, res, save);
		saveResponseHeaders(writer, context, res, save);
		saveRequestHeaders(writer, context, res, save);
		saveResponseData(writer, context, res, save);
		saveSamplerData(writer, context, res, save);
	}

	/**
	 * @param writer
	 * @param res
	 * @param save
	 */
	protected void saveSamplerData(HierarchicalStreamWriter writer, MarshallingContext context, SampleResult res,
			SampleSaveConfiguration save) {
		if (save.saveSamplerData(res)) {
			writeString(writer, TAG_SAMPLER_DATA, res.getSamplerData());
		}
        if (save.saveUrl()) {
            final URL url = res.getURL();
            if (url != null) {
            	writeItem(url, context, writer);
            }
        }
	}

	/**
	 * @param writer
	 * @param res
	 * @param save
	 */
	protected void saveResponseData(HierarchicalStreamWriter writer, MarshallingContext context, SampleResult res,
			SampleSaveConfiguration save) {
		if (save.saveResponseData(res)) {
			writer.startNode(TAG_RESPONSE_DATA);
            writer.addAttribute(ATT_CLASS, JAVA_LANG_STRING);
			try {
                if (SampleResult.TEXT.equals(res.getDataType())){
    				writer.setValue(new String(res.getResponseData(), res.getDataEncodingWithDefault()));
                }
                // Otherwise don't save anything - no point
			} catch (UnsupportedEncodingException e) {
				writer.setValue("Unsupported encoding in response data, can't record.");
			}
			writer.endNode();
		}
        if (save.saveFileName()){
            writer.startNode(TAG_RESPONSE_FILE);
            writer.addAttribute(ATT_CLASS, JAVA_LANG_STRING);
            writer.setValue(res.getResultFileName());
            writer.endNode();            
        }
	}

	/**
	 * @param writer
	 * @param res
	 * @param save
	 */
	protected void saveRequestHeaders(HierarchicalStreamWriter writer, MarshallingContext context, SampleResult res,
			SampleSaveConfiguration save) {
		if (save.saveRequestHeaders()) {
			writeString(writer, TAG_REQUEST_HEADER, res.getRequestHeaders());
		}
	}

	/**
	 * @param writer
	 * @param res
	 * @param save
	 */
	protected void saveResponseHeaders(HierarchicalStreamWriter writer, MarshallingContext context, SampleResult res,
			SampleSaveConfiguration save) {
		if (save.saveResponseHeaders()) {
			writeString(writer, TAG_RESPONSE_HEADER, res.getResponseHeaders());
		}
	}

	/**
	 * @param writer
	 * @param context
	 * @param res
	 * @param save
	 */
	protected void saveSubResults(HierarchicalStreamWriter writer, MarshallingContext context, SampleResult res,
			SampleSaveConfiguration save) {
		if (save.saveSubresults()) {
			SampleResult[] subResults = res.getSubResults();
			for (int i = 0; i < subResults.length; i++) {
				subResults[i].setSaveConfig(save);
				writeItem(subResults[i], context, writer);
			}
		}
	}

	/**
	 * @param writer
	 * @param context
	 * @param res
	 * @param save
	 */
	protected void saveAssertions(HierarchicalStreamWriter writer, MarshallingContext context, SampleResult res,
			SampleSaveConfiguration save) {
		if (save.saveAssertions()) {
			AssertionResult[] assertionResults = res.getAssertionResults();
			for (int i = 0; i < assertionResults.length; i++) {
				writeItem(assertionResults[i], context, writer);
			}
		}
	}

	/**
	 * @param writer
	 * @param res
	 * @param save
	 */
	protected void setAttributes(HierarchicalStreamWriter writer, MarshallingContext context, SampleResult res,
			SampleSaveConfiguration save) {
		if (save.saveTime()) {
			writer.addAttribute(ATT_TIME, Long.toString(res.getTime()));
		}
		if (save.saveLatency()) {
			writer.addAttribute(ATT_LATENCY, Long.toString(res.getLatency()));
		}
		if (save.saveTimestamp()) {
			writer.addAttribute(ATT_TIME_STAMP, Long.toString(res.getTimeStamp()));
		}
		if (save.saveSuccess()) {
			writer.addAttribute(ATT_SUCCESS, Boolean.toString(res.isSuccessful()));
		}
		if (save.saveLabel()) {
			writer.addAttribute(ATT_LABEL, ConversionHelp.encode(res.getSampleLabel()));
		}
		if (save.saveCode()) {
			writer.addAttribute(ATT_RESPONSE_CODE, ConversionHelp.encode(res.getResponseCode()));
		}
		if (save.saveMessage()) {
			writer.addAttribute(ATT_RESPONSE_MESSAGE, ConversionHelp.encode(res.getResponseMessage()));
		}
		if (save.saveThreadName()) {
			writer.addAttribute(ATT_THREADNAME, ConversionHelp.encode(res.getThreadName()));
		}
		if (save.saveDataType()) {
			writer.addAttribute(ATT_DATA_TYPE, ConversionHelp.encode(res.getDataType()));
		}
		if (save.saveEncoding()) {
			writer.addAttribute(ATT_DATA_ENCODING, ConversionHelp.encode(res.getDataEncodingNoDefault()));
		}
		if (save.saveBytes()) {
			writer.addAttribute(ATT_BYTES, String.valueOf(res.getBytes()));
		}
        if (save.saveSampleCount()){
        	writer.addAttribute(ATT_SAMPLE_COUNT, String.valueOf(res.getSampleCount()));
        	writer.addAttribute(ATT_ERROR_COUNT, String.valueOf(res.getErrorCount()));
        }
        if (save.saveThreadCounts()){
           writer.addAttribute(ATT_GRP_THRDS, String.valueOf(res.getGroupThreads()));
           writer.addAttribute(ATT_ALL_THRDS, String.valueOf(res.getAllThreads()));
        }
        SampleEvent event = (SampleEvent) context.get(SaveService.SAMPLE_EVENT_OBJECT);
        if (save.saveHostname()){
            if (event != null) {
            	writer.addAttribute(ATT_HOSTNAME, event.getHostname());        	
            }
        }
        if (event != null) {
        	for (int i = 0; i < SampleEvent.getVarCount(); i++){
        	   writer.addAttribute(SampleEvent.getVarName(i), ConversionHelp.encode(event.getVarValue(i)));
        	}
        }
	}

	/**
	 * @param writer
	 * @param tag
	 * @param value
	 */
	protected void writeString(HierarchicalStreamWriter writer, String tag, String value) {
		if (value != null) {
			writer.startNode(tag);
			writer.addAttribute(ATT_CLASS, JAVA_LANG_STRING);
			writer.setValue(value);
			writer.endNode();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.thoughtworks.xstream.converters.Converter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader,
	 *      com.thoughtworks.xstream.converters.UnmarshallingContext)
	 */
	public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
		SampleResult res = (SampleResult) createCollection(context.getRequiredType());
		retrieveAttributes(reader, context, res);
		while (reader.hasMoreChildren()) {
			reader.moveDown();
			Object subItem = readItem(reader, context, res);
			retrieveItem(reader, context, res, subItem);
			reader.moveUp();
		}

        // If we have a file, but no data, then read the file
        String resultFileName = res.getResultFileName();
        if (resultFileName.length()>0 
        &&  res.getResponseData().length == 0) {
            readFile(resultFileName,res);
        }
		return res;
	}

	/**
     * 
	 * @param reader
	 * @param context
	 * @param res
     * @return true if the item was processed (for HTTPResultConverter)
	 */
	protected boolean retrieveItem(HierarchicalStreamReader reader, UnmarshallingContext context, SampleResult res,
			Object subItem) {
		String nodeName = reader.getNodeName();
		if (subItem instanceof AssertionResult) {
			res.addAssertionResult((AssertionResult) subItem);
		} else if (subItem instanceof SampleResult) {
			res.storeSubResult((SampleResult) subItem);
		} else if (nodeName.equals(TAG_RESPONSE_HEADER)) {
			res.setResponseHeaders((String) subItem);
		} else if (nodeName.equals(TAG_REQUEST_HEADER)) {
			res.setRequestHeaders((String) subItem);
		} else if (nodeName.equals(TAG_RESPONSE_DATA)) {
			final String responseData = (String) subItem;
			if (responseData.length() > 0) {
				final String dataEncoding = res.getDataEncodingWithDefault();
				try {
					res.setResponseData(responseData.getBytes(dataEncoding));
				} catch (UnsupportedEncodingException e) {
					res.setResponseData(("Can't support the char set: " + dataEncoding).getBytes());
					res.setDataType(SampleResult.TEXT);
				}
			}
		} else if (nodeName.equals(TAG_SAMPLER_DATA)) {
			res.setSamplerData((String) subItem);
        } else if (nodeName.equals(TAG_RESPONSE_FILE)) {
            res.setResultFileName((String) subItem);
		// Don't try restoring the URL TODO: wy not?
		} else {
			return false;
		}
		return true;
	}

	/**
	 * @param reader
	 * @param res
	 */
	protected void retrieveAttributes(HierarchicalStreamReader reader, UnmarshallingContext context, SampleResult res) {
		res.setSampleLabel(ConversionHelp.decode(reader.getAttribute(ATT_LABEL)));
		res.setDataEncoding(ConversionHelp.decode(reader.getAttribute(ATT_DATA_ENCODING)));
		res.setDataType(ConversionHelp.decode(reader.getAttribute(ATT_DATA_TYPE)));
        String oldrc=reader.getAttribute(ATT_RESPONSE_CODE_OLD);
        if (oldrc!=null) {
            res.setResponseCode(ConversionHelp.decode(oldrc));
        } else {
            res.setResponseCode(ConversionHelp.decode(reader.getAttribute(ATT_RESPONSE_CODE)));
        }
		res.setResponseMessage(ConversionHelp.decode(reader.getAttribute(ATT_RESPONSE_MESSAGE)));
		res.setSuccessful(Converter.getBoolean(reader.getAttribute(ATT_SUCCESS), true));
		res.setThreadName(ConversionHelp.decode(reader.getAttribute(ATT_THREADNAME)));
		res.setStampAndTime(Converter.getLong(reader.getAttribute(ATT_TIME_STAMP)),
				Converter.getLong(reader.getAttribute(ATT_TIME)));
		res.setLatency(Converter.getLong(reader.getAttribute(ATT_LATENCY)));
		res.setBytes(Converter.getInt(reader.getAttribute(ATT_BYTES)));
		res.setSampleCount(Converter.getInt(reader.getAttribute(ATT_SAMPLE_COUNT),1)); // default is 1
		res.setErrorCount(Converter.getInt(reader.getAttribute(ATT_ERROR_COUNT),0)); // default is 0
		res.setGroupThreads(Converter.getInt(reader.getAttribute(ATT_GRP_THRDS)));
		res.setAllThreads(Converter.getInt(reader.getAttribute(ATT_ALL_THRDS)));
	}

    protected void readFile(String resultFileName, SampleResult res) {
        File in = null;
        FileInputStream fis = null;
        try {
            in = new File(resultFileName);
            fis = new FileInputStream(in);
            ByteArrayOutputStream outstream = new ByteArrayOutputStream(res.getBytes());
            byte[] buffer = new byte[4096];
            int len;
            while ((len = fis.read(buffer)) > 0) {
                outstream.write(buffer, 0, len);
            }
            outstream.close();
            res.setResponseData(outstream.toByteArray());
        } catch (FileNotFoundException e) {
            log.warn(e.getLocalizedMessage());
        } catch (IOException e) {
            log.warn(e.getLocalizedMessage());
        } finally {
            IOUtils.closeQuietly(fis);
        }
    }


	/**
	 * @param arg0
	 */
	public SampleResultConverter(Mapper arg0) {
		super(arg0);
	}
}