/*
 * 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.http.proxy;

import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.jmeter.assertions.ResponseAssertion;
import org.apache.jmeter.assertions.gui.AssertionGui;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.config.ConfigElement;
import org.apache.jmeter.config.ConfigTestElement;
import org.apache.jmeter.control.GenericController;
import org.apache.jmeter.control.gui.LogicControllerGui;
import org.apache.jmeter.engine.util.ValueReplacer;
import org.apache.jmeter.exceptions.IllegalUserActionException;
import org.apache.jmeter.functions.InvalidVariableException;
import org.apache.jmeter.gui.GuiPackage;
import org.apache.jmeter.gui.tree.JMeterTreeModel;
import org.apache.jmeter.gui.tree.JMeterTreeNode;
import org.apache.jmeter.protocol.http.control.HeaderManager;
import org.apache.jmeter.protocol.http.control.RecordingController;
import org.apache.jmeter.protocol.http.gui.HeaderPanel;
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
import org.apache.jmeter.samplers.SampleEvent;
import org.apache.jmeter.samplers.SampleListener;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.testelement.TestListener;
import org.apache.jmeter.testelement.TestPlan;
import org.apache.jmeter.testelement.WorkBench;
import org.apache.jmeter.testelement.property.BooleanProperty;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.IntegerProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jmeter.testelement.property.StringProperty;
import org.apache.jmeter.threads.ThreadGroup;
import org.apache.jmeter.timers.Timer;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
import org.apache.oro.text.MalformedCachePatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.Perl5Compiler;

//For unit tests, @see TestProxyControl

/**
 * Class handles storing of generated samples, etc
 */
public class ProxyControl extends GenericController {
    
    private static final Logger log = LoggingManager.getLoggerForClass();

    private static final String ASSERTION_GUI = AssertionGui.class.getName();

    private static final String LOGIC_CONTROLLER_GUI = LogicControllerGui.class.getName(); 

    private static final String HEADER_PANEL = HeaderPanel.class.getName(); 

	private transient Daemon server;

	public static final int DEFAULT_PORT = 8080;

    // and as a string
	public static final String DEFAULT_PORT_S =
        Integer.toString(DEFAULT_PORT);// Used by GUI

	//+ JMX file attributes
	private static final String PORT = "ProxyControlGui.port"; // $NON-NLS-1$

	private static final String EXCLUDE_LIST = "ProxyControlGui.exclude_list"; // $NON-NLS-1$

	private static final String INCLUDE_LIST = "ProxyControlGui.include_list"; // $NON-NLS-1$

	private static final String CAPTURE_HTTP_HEADERS = "ProxyControlGui.capture_http_headers"; // $NON-NLS-1$

	private static final String ADD_ASSERTIONS = "ProxyControlGui.add_assertion"; // $NON-NLS-1$

	private static final String GROUPING_MODE = "ProxyControlGui.grouping_mode"; // $NON-NLS-1$

	private static final String SAMPLER_TYPE_NAME = "ProxyControlGui.sampler_type_name"; // $NON-NLS-1$

	private static final String SAMPLER_REDIRECT_AUTOMATICALLY = "ProxyControlGui.sampler_redirect_automatically"; // $NON-NLS-1$

	private static final String SAMPLER_FOLLOW_REDIRECTS = "ProxyControlGui.sampler_follow_redirects"; // $NON-NLS-1$

	private static final String USE_KEEPALIVE = "ProxyControlGui.use_keepalive"; // $NON-NLS-1$

	private static final String SAMPLER_DOWNLOAD_IMAGES = "ProxyControlGui.sampler_download_images"; // $NON-NLS-1$

	private static final String REGEX_MATCH = "ProxyControlGui.regex_match"; // $NON-NLS-1$

	private static final String HTTPS_SPOOF = "ProxyControlGui.https_spoof"; // $NON-NLS-1$

	private static final String HTTPS_SPOOF_MATCH = "ProxyControlGui.https_spoof_match"; // $NON-NLS-1$

	private static final String CONTENT_TYPE_EXCLUDE = "ProxyControlGui.content_type_exclude"; // $NON-NLS-1$

	private static final String CONTENT_TYPE_INCLUDE = "ProxyControlGui.content_type_include"; // $NON-NLS-1$
	//- JMX file attributes
	
	public static final int GROUPING_NO_GROUPS = 0;

	public static final int GROUPING_ADD_SEPARATORS = 1;

	public static final int GROUPING_IN_CONTROLLERS = 2;

	public static final int GROUPING_STORE_FIRST_ONLY = 3;
	
	// Must agree with the order of entries in the drop-down 
	// created in ProxyControlGui.createHTTPSamplerPanel()
	public static final int SAMPLER_TYPE_HTTP_SAMPLER = 0;
	public static final int SAMPLER_TYPE_HTTP_SAMPLER2 = 1;

	private long lastTime = 0;// When was the last sample seen?

	private static final long sampleGap =
        JMeterUtils.getPropDefault("proxy.pause", 1000); // $NON-NLS-1$
    // Detect if user has pressed a new link

	private boolean addAssertions;

	private int groupingMode;
	
	private boolean samplerRedirectAutomatically;

	private boolean samplerFollowRedirects;
	
	private boolean useKeepAlive;

	private boolean samplerDownloadImages;

	private boolean regexMatch = false;// Should we match using regexes?
	
	/**
	 * Tree node where the samples should be stored.
	 * <p>
	 * This property is not persistent.
	 */
	private JMeterTreeNode target;

	public ProxyControl() {
		setPort(DEFAULT_PORT);
		setExcludeList(new HashSet());
		setIncludeList(new HashSet());
		setCaptureHttpHeaders(true); // maintain original behaviour
	}

	public void setPort(int port) {
		this.setProperty(new IntegerProperty(PORT, port));
	}

	public void setPort(String port) {
		setProperty(PORT, port);
	}

	public void setCaptureHttpHeaders(boolean capture) {
		setProperty(new BooleanProperty(CAPTURE_HTTP_HEADERS, capture));
	}

	public void setGroupingMode(int grouping) {
		this.groupingMode = grouping;
		setProperty(new IntegerProperty(GROUPING_MODE, grouping));
	}

	public void setAssertions(boolean b) {
		addAssertions = b;
		setProperty(new BooleanProperty(ADD_ASSERTIONS, b));
	}

	public void setSamplerTypeName(int samplerTypeName) {
		setProperty(new IntegerProperty(SAMPLER_TYPE_NAME, samplerTypeName));
	}

	public void setSamplerRedirectAutomatically(boolean b) {
		samplerRedirectAutomatically = b;
		setProperty(new BooleanProperty(SAMPLER_REDIRECT_AUTOMATICALLY, b));
	}

	public void setSamplerFollowRedirects(boolean b) {
		samplerFollowRedirects = b;
		setProperty(new BooleanProperty(SAMPLER_FOLLOW_REDIRECTS, b));
	}

	/**
	 * @param b
	 */
	public void setUseKeepAlive(boolean b) {
		useKeepAlive = b;
		setProperty(new BooleanProperty(USE_KEEPALIVE, b));
	}

	public void setSamplerDownloadImages(boolean b) {
		samplerDownloadImages = b;
		setProperty(new BooleanProperty(SAMPLER_DOWNLOAD_IMAGES, b));
	}

	public void setIncludeList(Collection list) {
		setProperty(new CollectionProperty(INCLUDE_LIST, new HashSet(list)));
	}

	public void setExcludeList(Collection list) {
		setProperty(new CollectionProperty(EXCLUDE_LIST, new HashSet(list)));
	}

	/**
	 * @param b
	 */
	public void setRegexMatch(boolean b) {
		regexMatch = b;
		setProperty(new BooleanProperty(REGEX_MATCH, b));
	}
	
	public void setHttpsSpoof(boolean b) {
		setProperty(new BooleanProperty(HTTPS_SPOOF, b));
	}
	
	public void setHttpsSpoofMatch(String s) {
		setProperty(new StringProperty(HTTPS_SPOOF_MATCH, s));
	}
	
	public void setContentTypeExclude(String contentTypeExclude) {
		setProperty(new StringProperty(CONTENT_TYPE_EXCLUDE, contentTypeExclude));
	}

	public void setContentTypeInclude(String contentTypeInclude) {
		setProperty(new StringProperty(CONTENT_TYPE_INCLUDE, contentTypeInclude));
	}

	public String getClassLabel() {
		return JMeterUtils.getResString("proxy_title"); // $NON-NLS-1$
	}

	public boolean getAssertions() {
		return getPropertyAsBoolean(ADD_ASSERTIONS);
	}

	public int getGroupingMode() {
		return getPropertyAsInt(GROUPING_MODE);
	}

	public int getPort() {
		return getPropertyAsInt(PORT);
	}

	public String getPortString() {
		return getPropertyAsString(PORT);
	}

	public int getDefaultPort() {
		return DEFAULT_PORT;
	}

	public boolean getCaptureHttpHeaders() {
		return getPropertyAsBoolean(CAPTURE_HTTP_HEADERS);
	}

	public int getSamplerTypeName() {
		return getPropertyAsInt(SAMPLER_TYPE_NAME);
	}
	
	public boolean getSamplerRedirectAutomatically() {
		return getPropertyAsBoolean(SAMPLER_REDIRECT_AUTOMATICALLY, false);
	}

	public boolean getSamplerFollowRedirects() {
		return getPropertyAsBoolean(SAMPLER_FOLLOW_REDIRECTS, true);
	}
	
	public boolean getUseKeepalive() {
		return getPropertyAsBoolean(USE_KEEPALIVE, true);
	}
	
	public boolean getSamplerDownloadImages() {
		return getPropertyAsBoolean(SAMPLER_DOWNLOAD_IMAGES, false);
	}

	public boolean getRegexMatch() {
		return getPropertyAsBoolean(REGEX_MATCH, false);
	}
	
	public boolean getHttpsSpoof() {
		return getPropertyAsBoolean(HTTPS_SPOOF, false);
	}
	
	public String getHttpsSpoofMatch() {
		return getPropertyAsString(HTTPS_SPOOF_MATCH, "");
	}
	
	public String getContentTypeExclude() {
		return getPropertyAsString(CONTENT_TYPE_EXCLUDE);
	}

	public String getContentTypeInclude() {
		return getPropertyAsString(CONTENT_TYPE_INCLUDE);
	}


	public Class getGuiClass() {
		return org.apache.jmeter.protocol.http.proxy.gui.ProxyControlGui.class;
	}

	public void addConfigElement(ConfigElement config) {
	}

	public void startProxy() {
		notifyTestListenersOfStart();
		server = new Daemon(getPort(), this);
		server.start();
	}

	public void addExcludedPattern(String pattern) {
		getExcludePatterns().addItem(pattern);
	}

	public CollectionProperty getExcludePatterns() {
		return (CollectionProperty) getProperty(EXCLUDE_LIST);
	}

	public void addIncludedPattern(String pattern) {
		getIncludePatterns().addItem(pattern);
	}

	public CollectionProperty getIncludePatterns() {
		return (CollectionProperty) getProperty(INCLUDE_LIST);
	}

	public void clearExcludedPatterns() {
		getExcludePatterns().clear();
	}

	public void clearIncludedPatterns() {
		getIncludePatterns().clear();
	}

	/**
	 * @return the target controller node
	 */
	public JMeterTreeNode getTarget() {
		return target;
	}

	/**
	 * Sets the target node where the samples generated by the proxy have to be
	 * stored.
	 */
	public void setTarget(JMeterTreeNode target) {
		this.target = target;
	}

	/**
	 * Receives the recorded sampler from the proxy server for placing in the
	 * test tree. param serverResponse to be added to allow saving of the
	 * server's response while recording. A future consideration.
	 */
	public synchronized void deliverSampler(HTTPSamplerBase sampler, TestElement[] subConfigs, SampleResult result) {
		if (filterContentType(result) && filterUrl(sampler)) {
			JMeterTreeNode myTarget = findTargetControllerNode();
			Collection defaultConfigurations = findApplicableElements(myTarget, ConfigTestElement.class, false);
			Collection userDefinedVariables = findApplicableElements(myTarget, Arguments.class, true);

			removeValuesFromSampler(sampler, defaultConfigurations);
			replaceValues(sampler, subConfigs, userDefinedVariables);
			sampler.setAutoRedirects(samplerRedirectAutomatically);
			sampler.setFollowRedirects(samplerFollowRedirects);
			sampler.setUseKeepAlive(useKeepAlive);
			sampler.setImageParser(samplerDownloadImages);

			placeSampler(sampler, subConfigs, myTarget);
		}
		else {
			if(log.isDebugEnabled()) {
				log.debug("Sample excluded based on url or content-type: " + result.getUrlAsString() + " - " + result.getContentType());
			}
			result.setSampleLabel("["+result.getSampleLabel()+"]");
		}
		// SampleEvent is not passed JMeterVariables, because they don't make sense for Proxy Recording
		notifySampleListeners(new SampleEvent(result, "WorkBench")); // TODO - is this the correct threadgroup name?
	}

	public void stopProxy() {
		if (server != null) {
			server.stopServer();
			try {
				server.join(1000); // wait for server to stop
			} catch (InterruptedException e) {
			}
			notifyTestListenersOfEnd();
			server = null;
		}
	}

    // Package protected to allow test case access
    boolean filterUrl(HTTPSamplerBase sampler) {
		String domain = sampler.getDomain();
		if (domain == null || domain.length() == 0) {
			return false;
		}

		String url = generateMatchUrl(sampler);
		CollectionProperty includePatterns = getIncludePatterns();
		if (includePatterns.size() > 0) {
			if (!matchesPatterns(url, includePatterns)) {
				return false;
			}
		}

		CollectionProperty excludePatterns = getExcludePatterns();
		if (excludePatterns.size() > 0) {
			if (matchesPatterns(url, excludePatterns)) {
				return false;
			}
		}

		return true;
	}

    // Package protected to allow test case access
    /**
     * Filter the response based on the content type.
     * If no include nor exclude filter is specified, the result will be included
     * 
     * @param result the sample result to check
     */
    boolean filterContentType(SampleResult result) {
    	String includeExp = getContentTypeInclude(); 
    	String excludeExp = getContentTypeExclude();
    	// If no expressions are specified, we let the sample pass
    	if((includeExp == null || includeExp.length() == 0) &&
    			(excludeExp == null || excludeExp.length() == 0)
    			)
    	{
    		return true;
    	}
    	
    	// Check that we have a content type
    	String sampleContentType = result.getContentType();    	
    	if(sampleContentType == null || sampleContentType.length() == 0) {
        	if(log.isDebugEnabled()) {
        		log.debug("No Content-type found for : " + result.getUrlAsString());
        	}
    		
    		return true;
    	}

    	if(log.isDebugEnabled()) {
    		log.debug("Content-type to filter : " + sampleContentType);
    	}
    	// Check if the include pattern is mathed
    	if(includeExp != null && includeExp.length() > 0) {
        	if(log.isDebugEnabled()) {
        		log.debug("Include expression : " + includeExp);
        	}    		
    		
    		Pattern pattern = null;
    		try {
    			pattern = JMeterUtils.getPatternCache().getPattern(includeExp, Perl5Compiler.READ_ONLY_MASK | Perl5Compiler.SINGLELINE_MASK);
    			if(!JMeterUtils.getMatcher().contains(sampleContentType, pattern)) {
    				return false;
    			}
    		} catch (MalformedCachePatternException e) {
    			log.warn("Skipped invalid content include pattern: " + includeExp, e);
    		}
    	}

    	// Check if the exclude pattern is mathed
    	if(excludeExp != null && excludeExp.length() > 0) {
        	if(log.isDebugEnabled()) {
        		log.debug("Exclude expression : " + excludeExp);
        	}

    		Pattern pattern = null;
    		try {
    			pattern = JMeterUtils.getPatternCache().getPattern(excludeExp, Perl5Compiler.READ_ONLY_MASK | Perl5Compiler.SINGLELINE_MASK);
    			if(JMeterUtils.getMatcher().contains(sampleContentType, pattern)) {
    				return false;
    			}
    		} catch (MalformedCachePatternException e) {
    			log.warn("Skipped invalid content exclude pattern: " + includeExp, e);
    		}
    	}

    	return true;
	}

	/*
	 * Helper method to add a Response Assertion
	 */
	private void addAssertion(JMeterTreeModel model, JMeterTreeNode node) throws IllegalUserActionException {
		ResponseAssertion ra = new ResponseAssertion();
		ra.setProperty(TestElement.GUI_CLASS, ASSERTION_GUI);
		ra.setName("Check response");
		ra.setTestFieldResponseData();
		model.addComponent(ra, node);
	}

	/*
	 * Helper method to add a Divider
	 */
	private void addDivider(JMeterTreeModel model, JMeterTreeNode node) throws IllegalUserActionException {
		GenericController sc = new GenericController();
		sc.setProperty(TestElement.GUI_CLASS, LOGIC_CONTROLLER_GUI);
		sc.setName("-------------------"); // $NON-NLS-1$
		model.addComponent(sc, node);
	}

	/**
	 * Helper method to add a Simple Controller to contain the samplers.
	 * 
	 * @param model
	 *            Test component tree model
	 * @param node
	 *            Node in the tree where we will add the Controller
	 * @param name
	 *            A name for the Controller
	 */
	private void addSimpleController(JMeterTreeModel model, JMeterTreeNode node, String name)
			throws IllegalUserActionException {
		GenericController sc = new GenericController();
		sc.setProperty(TestElement.GUI_CLASS, LOGIC_CONTROLLER_GUI);
		sc.setName(name);
		model.addComponent(sc, node);
	}

	/**
	 * Helpler method to replicate any timers found within the Proxy Controller
	 * into the provided sampler, while replacing any occurences of string _T_
	 * in the timer's configuration with the provided deltaT.
	 * 
	 * @param model
	 *            Test component tree model
	 * @param node
	 *            Sampler node in where we will add the timers
	 * @param deltaT
	 *            Time interval from the previous request
	 */
	private void addTimers(JMeterTreeModel model, JMeterTreeNode node, long deltaT) {
		TestPlan variables = new TestPlan();
		variables.addParameter("T", Long.toString(deltaT)); // $NON-NLS-1$
		ValueReplacer replacer = new ValueReplacer(variables);
		JMeterTreeNode mySelf = model.getNodeOf(this);
		Enumeration children = mySelf.children();
		while (children.hasMoreElements()) {
			JMeterTreeNode templateNode = (JMeterTreeNode) children.nextElement();
			if (templateNode.isEnabled()) {
				TestElement template = templateNode.getTestElement();
				if (template instanceof Timer) {
					TestElement timer = (TestElement) template.clone();
					try {
						replacer.undoReverseReplace(timer);
						model.addComponent(timer, node);
					} catch (InvalidVariableException e) {
						// Not 100% sure, but I believe this can't happen, so
						// I'll log and throw an error:
						log.error("Program error", e);
						throw new Error(e);
					} catch (IllegalUserActionException e) {
						// Not 100% sure, but I believe this can't happen, so
						// I'll log and throw an error:
						log.error("Program error", e);
						throw new Error(e);
					}
				}
			}
		}
	}

	/**
	 * Finds the first enabled node of a given type in the tree.
	 * 
	 * @param type
	 *            class of the node to be found
	 * 
	 * @return the first node of the given type in the test component tree, or
	 *         <code>null</code> if none was found.
	 */
	private JMeterTreeNode findFirstNodeOfType(Class type) {
		JMeterTreeModel treeModel = GuiPackage.getInstance().getTreeModel();
		List nodes = treeModel.getNodesOfType(type);
		Iterator iter = nodes.iterator();
		while (iter.hasNext()) {
			JMeterTreeNode node = (JMeterTreeNode) iter.next();
			if (node.isEnabled()) {
				return node;
			}
		}
		return null;
	}

	/**
	 * Finds the controller where samplers have to be stored, that is:
	 * <ul>
	 * <li>The controller specified by the <code>target</code> property.
	 * <li>If none was specified, the first RecordingController in the tree.
	 * <li>If none is found, the first ThreadGroup in the tree.
	 * <li>If none is found, the Workspace.
	 * </ul>
	 * 
	 * @return the tree node for the controller where the proxy must store the
	 *         generated samplers.
	 */
	private JMeterTreeNode findTargetControllerNode() {
		JMeterTreeNode myTarget = getTarget();
		if (myTarget != null) {
			return myTarget;
		}
		myTarget = findFirstNodeOfType(RecordingController.class);
		if (myTarget != null) {
			return myTarget;
		}
		myTarget = findFirstNodeOfType(ThreadGroup.class);
		if (myTarget != null) {
			return myTarget;
		}
		myTarget = findFirstNodeOfType(WorkBench.class);
		if (myTarget != null) {
			return myTarget;
		}
		log.error("Program error: proxy recording target not found.");
		return null;
	}

	/**
	 * Finds all configuration objects of the given class applicable to the
	 * recorded samplers, that is:
	 * <ul>
	 * <li>All such elements directly within the HTTP Proxy Server (these have
	 * the highest priority).
	 * <li>All such elements directly within the target controller (higher
	 * priority) or directly within any containing controller (lower priority),
	 * including the Test Plan itself (lowest priority).
	 * </ul>
	 * 
	 * @param myTarget
	 *            tree node for the recording target controller.
	 * @param myClass
	 *            Class of the elements to be found.
	 * @param ascending
	 *            true if returned elements should be ordered in ascending
	 *            priority, false if they should be in descending priority.
	 * 
	 * @return a collection of applicable objects of the given class.
	 */
	private Collection findApplicableElements(JMeterTreeNode myTarget, Class myClass, boolean ascending) {
		JMeterTreeModel treeModel = GuiPackage.getInstance().getTreeModel();
		LinkedList elements = new LinkedList();

		// Look for elements directly within the HTTP proxy:
		Enumeration kids = treeModel.getNodeOf(this).children();
		while (kids.hasMoreElements()) {
			JMeterTreeNode subNode = (JMeterTreeNode) kids.nextElement();
			if (subNode.isEnabled()) {
				TestElement element = (TestElement) subNode.getUserObject();
				if (myClass.isInstance(element)) {
					if (ascending) {
						elements.addFirst(element);
					} else {
						elements.add(element);
					}
				}
			}
		}

		// Look for arguments elements in the target controller or higher up:
		for (JMeterTreeNode controller = myTarget; controller != null; controller = (JMeterTreeNode) controller
				.getParent()) {
			kids = controller.children();
			while (kids.hasMoreElements()) {
				JMeterTreeNode subNode = (JMeterTreeNode) kids.nextElement();
				if (subNode.isEnabled()) {
					TestElement element = (TestElement) subNode.getUserObject();
					if (myClass.isInstance(element)) {
						log.debug("Applicable: " + element.getName());
						if (ascending) {
							elements.addFirst(element);
						} else {
							elements.add(element);
						}
					}

					// Special case for the TestPlan's Arguments sub-element:
					if (element instanceof TestPlan) {
						TestPlan tp = (TestPlan) element;
						Arguments args = tp.getArguments();
						if (myClass.isInstance(args)) {
							if (ascending) {
								elements.addFirst(args);
							} else {
								elements.add(args);
							}
						}
					}
				}
			}
		}

		return elements;
	}

	private void placeSampler(HTTPSamplerBase sampler, TestElement[] subConfigs, JMeterTreeNode myTarget) {
		try {
			JMeterTreeModel treeModel = GuiPackage.getInstance().getTreeModel();

			boolean firstInBatch = false;
			long now = System.currentTimeMillis();
			long deltaT = now - lastTime;
			if (deltaT > sampleGap) {
				if (!myTarget.isLeaf() && groupingMode == GROUPING_ADD_SEPARATORS) {
					addDivider(treeModel, myTarget);
				}
				if (groupingMode == GROUPING_IN_CONTROLLERS) {
					addSimpleController(treeModel, myTarget, sampler.getName());
				}
				firstInBatch = true;// Remember this was first in its batch
			}
			if (lastTime == 0) {
				deltaT = 0; // Decent value for timers
			}
			lastTime = now;

			if (groupingMode == GROUPING_STORE_FIRST_ONLY) {
				if (!firstInBatch) {
					return; // Huh! don't store this one!
				}

				// If we're not storing subsequent samplers, we'll need the
				// first sampler to do all the work...:
				sampler.setFollowRedirects(true);
				sampler.setImageParser(true);
			}

			if (groupingMode == GROUPING_IN_CONTROLLERS) {
				// Find the last controller in the target to store the
				// sampler there:
				for (int i = myTarget.getChildCount() - 1; i >= 0; i--) {
					JMeterTreeNode c = (JMeterTreeNode) myTarget.getChildAt(i);
					if (c.getTestElement() instanceof GenericController) {
						myTarget = c;
						break;
					}
				}
			}

			JMeterTreeNode newNode = treeModel.addComponent(sampler, myTarget);

			if (firstInBatch) {
				if (addAssertions) {
					addAssertion(treeModel, newNode);
				}
				addTimers(treeModel, newNode, deltaT);
				firstInBatch = false;
			}

			for (int i = 0; subConfigs != null && i < subConfigs.length; i++) {
				if (subConfigs[i] instanceof HeaderManager) {
					subConfigs[i].setProperty(TestElement.GUI_CLASS, HEADER_PANEL);
					treeModel.addComponent(subConfigs[i], newNode);
				}
			}
		} catch (IllegalUserActionException e) {
			JMeterUtils.reportErrorToUser(e.getMessage());
		}
	}

	/**
	 * Remove from the sampler all values which match the one provided by the
	 * first configuration in the given collection which provides a value for
	 * that property.
	 * 
	 * @param sampler
	 *            Sampler to remove values from.
	 * @param configurations
	 *            ConfigTestElements in descending priority.
	 */
	private void removeValuesFromSampler(HTTPSamplerBase sampler, Collection configurations) {
		for (PropertyIterator props = sampler.propertyIterator(); props.hasNext();) {
			JMeterProperty prop = props.next();
			String name = prop.getName();
			String value = prop.getStringValue();

			// There's a few properties which are excluded from this processing:
			if (name.equals(TestElement.ENABLED) || name.equals(TestElement.GUI_CLASS) || name.equals(TestElement.NAME)
					|| name.equals(TestElement.TEST_CLASS)) {
				continue; // go on with next property.
			}

			for (Iterator configs = configurations.iterator(); configs.hasNext();) {
				ConfigTestElement config = (ConfigTestElement) configs.next();

				String configValue = config.getPropertyAsString(name);

				if (configValue != null && configValue.length() > 0) {
					if (configValue.equals(value)) {
						sampler.setProperty(name, ""); // $NON-NLS-1$
					}
					// Property was found in a config element. Whether or not
					// it matched the value in the sampler, we're done with
					// this property -- don't look at lower-priority configs:
					break;
				}
			}
		}
	}

	private String generateMatchUrl(HTTPSamplerBase sampler) {
		StringBuffer buf = new StringBuffer(sampler.getDomain());
		buf.append(':'); // $NON-NLS-1$
		buf.append(sampler.getPort());
		buf.append(sampler.getPath());
		if (sampler.getQueryString().length() > 0) {
			buf.append('?'); // $NON-NLS-1$
			buf.append(sampler.getQueryString());
		}
		return buf.toString();
	}

	private boolean matchesPatterns(String url, CollectionProperty patterns) {
		PropertyIterator iter = patterns.iterator();
		while (iter.hasNext()) {
			String item = iter.next().getStringValue();
			Pattern pattern = null;
			try {
				pattern = JMeterUtils.getPatternCache().getPattern(item, Perl5Compiler.READ_ONLY_MASK | Perl5Compiler.SINGLELINE_MASK);
				if (JMeterUtils.getMatcher().matches(url, pattern)) {
					return true;
				}
			} catch (MalformedCachePatternException e) {
				log.warn("Skipped invalid pattern: " + item, e);
			}
		}
		return false;
	}

	/**
	 * Scan all test elements passed in for values matching the value of any of
	 * the variables in any of the variable-holding elements in the collection.
	 * 
	 * @param sampler
	 *            A TestElement to replace values on
	 * @param configs
	 *            More TestElements to replace values on
	 * @param variables
	 *            Collection of Arguments to use to do the replacement, ordered
	 *            by ascending priority.
	 */
	private void replaceValues(TestElement sampler, TestElement[] configs, Collection variables) {
		// Build the replacer from all the variables in the collection:
		ValueReplacer replacer = new ValueReplacer();
		for (Iterator vars = variables.iterator(); vars.hasNext();) {
			replacer.addVariables(((Arguments) vars.next()).getArgumentsAsMap());
		}

		try {
			replacer.reverseReplace(sampler, regexMatch);
			for (int i = 0; i < configs.length; i++) {
				if (configs[i] != null) {
					replacer.reverseReplace(configs[i], regexMatch);
				}

			}
		} catch (InvalidVariableException e) {
			log.warn("Invalid variables included for replacement into recorded " + "sample", e);
		}
	}

	/**
	 * This will notify sample listeners directly within the Proxy of the
	 * sampling that just occured -- so that we have a means to record the
	 * server's responses as we go.
	 * 
	 * @param event
	 *            sampling event to be delivered
	 */
	private void notifySampleListeners(SampleEvent event) {
		JMeterTreeModel treeModel = GuiPackage.getInstance().getTreeModel();
		JMeterTreeNode myNode = treeModel.getNodeOf(this);
		Enumeration kids = myNode.children();
		while (kids.hasMoreElements()) {
			JMeterTreeNode subNode = (JMeterTreeNode) kids.nextElement();
			if (subNode.isEnabled()) {
				TestElement testElement = subNode.getTestElement();
				if (testElement instanceof SampleListener) {
					((SampleListener) testElement).sampleOccurred(event);
				}
			}
		}
	}

	/**
	 * This will notify test listeners directly within the Proxy that the 'test'
	 * (here meaning the proxy recording) has started.
	 */
	private void notifyTestListenersOfStart() {
		JMeterTreeModel treeModel = GuiPackage.getInstance().getTreeModel();
		JMeterTreeNode myNode = treeModel.getNodeOf(this);
		Enumeration kids = myNode.children();
		while (kids.hasMoreElements()) {
			JMeterTreeNode subNode = (JMeterTreeNode) kids.nextElement();
			if (subNode.isEnabled()) {
				TestElement testElement = subNode.getTestElement();
				if (testElement instanceof TestListener) {
					((TestListener) testElement).testStarted();
				}
			}
		}
	}

	/**
	 * This will notify test listeners directly within the Proxy that the 'test'
	 * (here meaning the proxy recording) has ended.
	 */
	private void notifyTestListenersOfEnd() {
		JMeterTreeModel treeModel = GuiPackage.getInstance().getTreeModel();
		JMeterTreeNode myNode = treeModel.getNodeOf(this);
		Enumeration kids = myNode.children();
		while (kids.hasMoreElements()) {
			JMeterTreeNode subNode = (JMeterTreeNode) kids.nextElement();
			if (subNode.isEnabled()) {
				TestElement testElement = subNode.getTestElement();
				if (testElement instanceof TestListener) {
					((TestListener) testElement).testEnded();
				}
			}
		}
	}

	public boolean canRemove() {
		return null == server;
	}
}
