/*
 * 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.protocol.system;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;

import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Argument;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.config.ConfigTestElement;
import org.apache.jmeter.samplers.AbstractSampler;
import org.apache.jmeter.samplers.Entry;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.services.FileServer;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.property.TestElementProperty;
import org.apache.jorphan.exec.SystemCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A sampler for executing a System function.
 */
public class SystemSampler extends AbstractSampler {

    private static final long serialVersionUID = 1;

    // + JMX names, do not change their values
    public static final String COMMAND = "SystemSampler.command";

    public static final String DIRECTORY = "SystemSampler.directory";

    public static final String ARGUMENTS = "SystemSampler.arguments";

    public static final String ENVIRONMENT = "SystemSampler.environment";

    public static final String CHECK_RETURN_CODE = "SystemSampler.checkReturnCode";

    public static final String EXPECTED_RETURN_CODE = "SystemSampler.expectedReturnCode";

    private static final String STDOUT = "SystemSampler.stdout";

    private static final String STDERR = "SystemSampler.stderr";

    private static final String STDIN = "SystemSampler.stdin";

    private static final String TIMEOUT = "SystemSampler.timeout";

    // - JMX names

    /**
     * Logging
     */
    private static final Logger log = LoggerFactory.getLogger(SystemSampler.class);

    private static final Set<String> APPLIABLE_CONFIG_CLASSES = new HashSet<>(
            Arrays.asList("org.apache.jmeter.config.gui.SimpleConfigGui"));

    public static final int DEFAULT_RETURN_CODE = 0;


    /**
     * Create a SystemSampler.
     */
    public SystemSampler() {
        super();
    }

    /**
     * Performs a test sample.
     *
     * @param entry
     *            the Entry for this sample
     * @return test SampleResult
     */
    @Override
    public SampleResult sample(Entry entry) {
        SampleResult results = new SampleResult();
        results.setDataType(SampleResult.TEXT);
        results.setSampleLabel(getName());

        String command = getCommand();
        Arguments args = getArguments();
        Arguments environment = getEnvironmentVariables();
        boolean checkReturnCode = getCheckReturnCode();
        int expectedReturnCode = getExpectedReturnCode();
        List<String> cmds = new ArrayList<>(args.getArgumentCount() + 1);
        StringBuilder cmdLine = new StringBuilder((null == command) ? "" : command);
        cmds.add(command);
        for (int i=0;i<args.getArgumentCount();i++) {
            Argument arg = args.getArgument(i);
            cmds.add(arg.getPropertyAsString(Argument.VALUE));
            cmdLine.append(" ");
            cmdLine.append(cmds.get(i+1));
        }

        Map<String,String> env = new HashMap<>();
        for (int i=0;i<environment.getArgumentCount();i++) {
            Argument arg = environment.getArgument(i);
            env.put(arg.getName(), arg.getPropertyAsString(Argument.VALUE));
        }

        File directory;
        if(StringUtils.isEmpty(getDirectory())) {
            directory = new File(FileServer.getDefaultBase());
            if(log.isDebugEnabled()) {
                log.debug("Using default directory:"+directory.getAbsolutePath());
            }
        } else {
            directory = new File(getDirectory());
            if(log.isDebugEnabled()) {
                log.debug("Using configured directory:"+directory.getAbsolutePath());
            }
        }

        if(log.isDebugEnabled()) {
            log.debug("Will run : "+cmdLine + " using working directory:"+directory.getAbsolutePath()+
                    " with environment: "+env);
        }

        results.setSamplerData("Working Directory: "+directory.getAbsolutePath()+
                "\nEnvironment: "+env+
                "\nExecuting: " + cmdLine.toString());

        SystemCommand nativeCommand = null;
        try {
            nativeCommand = new SystemCommand(directory, getTimeout(), 0, env, getStdin(), getStdout(), getStderr());
            results.sampleStart();
            int returnCode = nativeCommand.run(cmds);
            results.sampleEnd();
            results.setResponseCode(Integer.toString(returnCode));
            if(log.isDebugEnabled()) {
                log.debug("Ran : "+cmdLine + " using working directory: "+directory.getAbsolutePath()+
                        " with execution environment: "+nativeCommand.getExecutionEnvironment()+ " => " + returnCode);
            }

            if (checkReturnCode && (returnCode != expectedReturnCode)) {
                results.setSuccessful(false);
                results.setResponseMessage("Unexpected return code.  Expected ["+expectedReturnCode+"]. Actual ["+returnCode+"].");
            } else {
                results.setSuccessful(true);
                results.setResponseMessage("OK");
            }
        } catch (IOException ioe) {
            results.sampleEnd();
            results.setSuccessful(false);
            results.setResponseCode("500"); //$NON-NLS-1$
            results.setResponseMessage("Exception occurred whilst executing system call: " + ioe);
        } catch (InterruptedException ie) {
            results.sampleEnd();
            results.setSuccessful(false);
            results.setResponseCode("500"); //$NON-NLS-1$
            results.setResponseMessage("System Sampler interrupted whilst executing system call: " + ie);
            Thread.currentThread().interrupt();
        } catch (TimeoutException e) {
            results.sampleEnd();
            results.setSuccessful(false);
            results.setResponseCode("500");
            results.setResponseMessage(e.getMessage());
        }

        if (nativeCommand != null) {
            results.setResponseData(nativeCommand.getOutResult().getBytes()); // default charset is deliberate here
        }

        return results;
    }

    /**
     * @see org.apache.jmeter.samplers.AbstractSampler#applies(org.apache.jmeter.config.ConfigTestElement)
     */
    @Override
    public boolean applies(ConfigTestElement configElement) {
        String guiClass = configElement.getProperty(TestElement.GUI_CLASS).getStringValue();
        return APPLIABLE_CONFIG_CLASSES.contains(guiClass);
    }

    /**
     * @return working directory to use for system commands
     */
    public String getDirectory() {
        return getPropertyAsString(DIRECTORY, FileServer.getDefaultBase());
    }

    /**
     * Set the working directory to use for system commands
     *
     * @param directory
     *            working directory to use for system commands
     */
    public void setDirectory(String directory) {
        setProperty(DIRECTORY, directory, FileServer.getDefaultBase());
    }

    /**
     * Sets the Command attribute of the JavaConfig object
     *
     * @param command
     *            the new Command value
     */
    public void setCommand(String command) {
        setProperty(COMMAND, command);
    }

    /**
     * Gets the Command attribute of the JavaConfig object
     *
     * @return the Command value
     */
    public String getCommand() {
        return getPropertyAsString(COMMAND);
    }

    /**
     * Set the arguments (parameters) for the JavaSamplerClient to be executed
     * with.
     *
     * @param args
     *            the new arguments. These replace any existing arguments.
     */
    public void setArguments(Arguments args) {
        setProperty(new TestElementProperty(ARGUMENTS, args));
    }

    /**
     * Get the arguments (parameters) for the JavaSamplerClient to be executed
     * with.
     *
     * @return the arguments
     */
    public Arguments getArguments() {
        return (Arguments) getProperty(ARGUMENTS).getObjectValue();
    }

    /**
     * @param checkit boolean indicates if we check or not return code
     */
    public void setCheckReturnCode(boolean checkit) {
        setProperty(CHECK_RETURN_CODE, checkit);
    }

    /**
     * @return boolean indicating if we check or not return code
     */
    public boolean getCheckReturnCode() {
        return getPropertyAsBoolean(CHECK_RETURN_CODE);
    }

    /**
     * @param code expected return code
     */
    public void setExpectedReturnCode(int code) {
        setProperty(EXPECTED_RETURN_CODE, Integer.toString(code));
    }

    /**
     * @return expected return code
     */
    public int getExpectedReturnCode() {
        return getPropertyAsInt(EXPECTED_RETURN_CODE);
    }

    /**
     * @param arguments Env vars
     */
    public void setEnvironmentVariables(Arguments arguments) {
        setProperty(new TestElementProperty(ENVIRONMENT, arguments));
    }

    /**
     * Get the env variables
     *
     * @return the arguments
     */
    public Arguments getEnvironmentVariables() {
        return (Arguments) getProperty(ENVIRONMENT).getObjectValue();
    }

    public String getStdout() {
        return getPropertyAsString(STDOUT, "");
    }

    public void setStdout(String filename) {
        setProperty(STDOUT, filename, "");
    }

    public String getStderr() {
        return getPropertyAsString(STDERR, "");
    }

    public void setStderr(String filename) {
        setProperty(STDERR, filename, "");
    }

    public String getStdin() {
        return getPropertyAsString(STDIN, "");
    }

    public void setStdin(String filename) {
        setProperty(STDIN, filename, "");
    }

    public long getTimeout() {
        return getPropertyAsLong(TIMEOUT, 0L);
    }

    public void setTimout(long timeoutMs) {
        setProperty(TIMEOUT, timeoutMs, 0L);
    }
}
