// Copyright � 2002-2007 Canoo Engineering AG, Switzerland.
package com.canoo.webtest.steps;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Task;
import com.canoo.webtest.ant.WebtestTask;
import com.canoo.webtest.engine.Context;
import com.canoo.webtest.engine.EqualsStringVerfier;
import com.canoo.webtest.engine.IStringVerifier;
import com.canoo.webtest.engine.RegExStringVerifier;
import com.canoo.webtest.engine.StepExecutionException;
import com.canoo.webtest.interfaces.IComputeValue;
import com.canoo.webtest.interfaces.IVerificationStep;
import com.canoo.webtest.reporting.IStepResultListener;
import com.canoo.webtest.util.Checker;
import com.canoo.webtest.util.MapUtil;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.ScriptException;
/**
* Abstract superclass for all test steps. Provides generic services for all subclasses.
*
* @author Carsten Seibert
* @author Marc Guillemot
* @author Paul King, ASERT
* @webtest.step
*/
public abstract class Step extends Task implements Serializable, Cloneable {
private static final Logger LOG = Logger.getLogger(Step.class);
public static final String ELEMENT_ATTRIBUTE_NAME = "name";
public static final String ELEMENT_ATTRIBUTE_ID = "id";
/**
* @deprecated The default is now not to set the value at all.
*/
public static final String DEFAULT_DESCRIPTION = "<unknown>";
private Date fStarted;
private Date fCompleted;
private boolean fSuccessful;
/**
* The property type is set according to webtest's <em>defaultPropertyType</em>.
*/
public static final String PROPERTY_TYPE_DEFAULT = null;
/**
* The property is a <em>dynamic</em> property.
*/
public static final String PROPERTY_TYPE_DYNAMIC = "dynamic";
/**
* The property is an <em>ant</em> property.
*/
public static final String PROPERTY_TYPE_ANT = "ant";
/**
* The property is an <em>ant</em> property that must not overwrite an existing property.
*/
public static final String PROPERTY_TYPE_ANT_STRICT = "antstrict";
/**
* This is the abstract base class for all test step specifications.
*/
protected Step() {
}
private void checkSetup() {
Checker.assertNonNull(getProject(), "Project cannot be null");
}
public Context getContext() {
return WebtestTask.getThreadContext();
}
/**
* Called to perform the step's functionality.
* Before calling this method, the step has been completely initialized (including expanding and verifying the parameters)
* and the environment notified of the start.
*
* @throws Exception
*/
public abstract void doExecute() throws Exception;
/**
* Ant calls this method to invoke this task's functionality.
* We expand and verify the steps parameters then call doExecute()
* as well as handle lifecycle notifications.
*/
public void execute() {
//expandProperties(); FIXME: use PropertyHelper!
checkContextDefined();
notifyStarted();
try {
verifyParameters();
doExecute();
// give time for background js tasks to finish when needed
final Context context = getContext();
final Page currentPage = context.getCurrentResponse();
if (!(this instanceof IVerificationStep) && currentPage != null && context.getConfig().isEasyAjax()) {
currentPage.getEnclosingWindow().getThreadManager().joinAll(context.getConfig().getEasyAjaxDelay());
}
final ScriptException bgException = context.getBackgroundJSError();
if (bgException != null) {
handleException(bgException);
}
}
catch (final Exception ex) {
handleException(ex);
}
finally {
notifyCompleted();
notifyStepResultsListeners();
}
notifySuccess();
}
private void checkContextDefined() {
if (WebtestTask.getThreadContext() == null) {
throw new StepExecutionException("Step not inside a webtest", this);
}
}
/**
* Notifies the interested project build listeners (typically only the {@link com.canoo.webtest.reporting.StepExecutionListener})
* that this step has produced results
*/
protected void notifyStepResultsListeners() {
final Map results = getComputedParameters();
if (results.isEmpty()) {
LOG.debug("Step didn't produce results, no need to notifying listeners");
return;
}
for (final Iterator iter = getProject().getBuildListeners().iterator(); iter.hasNext();) {
final BuildListener listener = (BuildListener) iter.next();
if (listener instanceof IStepResultListener) {
LOG.debug("Notifying " + listener + " of " + results.size() + " results");
((IStepResultListener) listener).stepResults(results);
}
}
}
/**
* Called to let the step's implementation validate its parameters.
* The method is called after parameter extensions but before {@link #doExecute()}.
* This implementation does nothing, overwrite as needed.
*/
protected void verifyParameters() {
// default is do nothing
}
public boolean hasDescription() {
return StringUtils.isNotEmpty(getDescription());
}
/**
* Gets the description with a prefix and suffix if the description is set.
*
* @param prefix A string to display before the description.
* @param suffix A string to display after the description.
* @return the concatenation of prefix, description and suffix.
*/
public String getDescription(final String prefix, final String suffix) {
final String description = getDescription();
if (!StringUtils.isNotEmpty(description)) {
return "";
}
return prefix + description + suffix;
}
/**
* Gets the execution time for a completed step.
*
* @return the execution time in ms
*/
public long getDuration() {
return fCompleted.getTime() - fStarted.getTime();
}
protected String getStepLabel() {
return "Step[" + getStepLabelBrief() + "]";
}
private String getStepLabelBrief() {
final StringBuffer message = new StringBuffer();
message.append(getTaskName());
message.append(getDescription(" \"", "\""));
message.append(" (").append(getContext().getCurrentStepNumber()).append("/");
message.append(getContext().getNumberOfSteps()).append(")");
return message.toString();
}
public boolean isCompleted() {
return fStarted != null && fCompleted != null;
}
public boolean isStarted() {
return fStarted != null;
}
public boolean isSuccessful() {
return fSuccessful;
}
/**
* Called after {@link #doExecute()} has completed (successfully or not)
*/
public void notifyCompleted() {
fCompleted = new Date();
LOG.debug("Completed Step: " + getStepLabelBrief());
}
/**
* Called before calling {@link #doExecute()}
*/
public void notifyStarted() {
fStarted = new Date();
LOG.info(">>>> Start Step: " + getStepLabelBrief());
}
public void notifySuccess() {
fSuccessful = true;
LOG.debug("<<<< Successful Step: " + getStepLabelBrief());
}
/**
* @param description
* @deprecated since June 10 2005. Use {@link Task#setDescription(String)}
* (setter should not be removed for compatibility with existing test sequences)
*/
public void setStepid(final String description) {
LOG.warn("'stepid' is deprecated - use 'description' instead");
setDescription(description);
}
public String toString() {
final StringBuffer sb = new StringBuffer(64);
sb.append(ClassUtils.getShortClassName(getClass()));
sb.append(" at ");
sb.append(getLocation().toString());
sb.append(" with (");
final Map parms = getParameterDictionary();
for (final Iterator iter = parms.keySet().iterator(); iter.hasNext(); sb.append(", ")) {
final Object param = iter.next();
sb.append(param).append("=\"").append(parms.get(param)).append("\"");
}
if (!parms.isEmpty()) {
sb.setLength(sb.length() - 2);
}
sb.append(")");
return sb.toString();
}
protected String getDefaultPropertyType() {
return getContext().getConfig().getDefaultPropertyType();
}
/**
* Sets a property of the default type.
*
* @param name The name of the property.
* @param value The value of the property.
*/
public void setWebtestProperty(final String name, final String value) {
setWebtestProperty(name, value, null);
}
/**
* Sets a property of the default type.
*
* @param name The name of the property.
* @param value The value of the property.
* @param propertyType The kind of property desired. One of {@link #PROPERTY_TYPE_ANT},
* {@link #PROPERTY_TYPE_ANT_STRICT}, {@link #PROPERTY_TYPE_DYNAMIC} or {@link #PROPERTY_TYPE_DEFAULT}.
*/
public void setWebtestProperty(final String name, final String value, final String propertyType) {
final String thisPropType = propertyType == PROPERTY_TYPE_DEFAULT ? getDefaultPropertyType() : propertyType;
LOG.debug("setWebtestProperty: " + name + "=" + value + " [" + thisPropType + "]");
if (StringUtils.isEmpty(thisPropType) || PROPERTY_TYPE_DYNAMIC.equals(thisPropType)) {
getContext().getWebtest().setDynamicProperty(name, value);
return;
}
if (PROPERTY_TYPE_ANT.equals(thisPropType)) {
checkSetup();
getProject().setProperty(name, value);
return;
}
if (PROPERTY_TYPE_ANT_STRICT.equals(thisPropType)) {
checkSetup();
getProject().setNewProperty(name, value);
return;
}
throw new StepExecutionException("Unknown propertyType: " + thisPropType, this);
}
/**
* Gets a property of the default type.
*
* @param name The name of the property.
* @return The value of the property.
*/
public String getWebtestProperty(final String name) {
return getWebtestProperty(name, PROPERTY_TYPE_DEFAULT);
}
/**
* Gets a property of the specified type
*
* @param name The name of the property.
* @param propertyType The kind of property desired. One of {@link #PROPERTY_TYPE_ANT},
* {@link #PROPERTY_TYPE_DYNAMIC} or {@link #PROPERTY_TYPE_DEFAULT}.
* @return The value of the property.
*/
public String getWebtestProperty(final String name, final String propertyType) {
final String thisPropType = propertyType == PROPERTY_TYPE_DEFAULT ? getDefaultPropertyType() : propertyType;
LOG.debug("getWebtestProperty(" + name + ") [" + thisPropType + "]");
if (StringUtils.isEmpty(thisPropType) || PROPERTY_TYPE_DYNAMIC.equals(thisPropType)) {
return getContext().getWebtest().getDynamicProperty(name);
}
if (thisPropType.startsWith(PROPERTY_TYPE_ANT)) {
checkSetup();
return getProject().getProperty(name);
}
throw new StepExecutionException("Unknown propertyType: " + thisPropType, this);
}
public Map getWebtestProperties() {
return getWebtestProperties(null);
}
public Map getWebtestProperties(final String propertyType) {
final String thisPropType = propertyType == null ? getDefaultPropertyType() : propertyType;
if (StringUtils.isEmpty(thisPropType) || PROPERTY_TYPE_DYNAMIC.equals(thisPropType)) {
return getContext().getWebtest().getDynamicProperties();
}
if (thisPropType.startsWith(PROPERTY_TYPE_ANT)) {
checkSetup();
return getProject().getProperties();
}
throw new StepExecutionException("Unknown propertyType: " + thisPropType, this);
}
/**
* This creates a bitwise copy of the receiver. Since we do not reference
* any complex objects as attributes, the default implementation of
* object will do.
* The mere relay to the super implementation is left in the code as a
* reminder that this needs to be updated as soon as complex objects
* are aggregated right here or in a subclass.
*/
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
protected static IStringVerifier getVerifier(final boolean useRegex) {
return useRegex ? RegExStringVerifier.INSTANCE : EqualsStringVerfier.INSTANCE;
}
/**
* Called if {@link #doExecute()} throws an exception
* @param t the thrown exception
*/
protected void handleException(final Throwable t) {
LOG.debug("Handling exception " + t.getClass().getName() + ": " + t.getMessage(), t);
StepUtil.handleException(t);
}
/**
* Throw an exception if the condition holds.
*
* @param condition If true, throws the exception.
* @param message The error message.
*/
protected void paramCheck(final boolean condition, final String message) {
if (condition) {
throw new StepExecutionException(message, this);
}
}
protected void nullParamCheck(final Object param, final String paramName) {
paramCheck(param == null, "Required parameter \"" + paramName + "\" not set!");
}
protected void emptyParamCheck(final String param, final String paramName) {
paramCheck(StringUtils.isEmpty(param),
"Required parameter \"" + paramName + "\" not set or set to empty string!");
}
/**
* Checks that the parameter's value is non negative
*
* @param paramName the name of the parameter
* @param value the parameter value
* @throws StepExecutionException if the value is negative
*/
protected void positiveOrZeroParamCheck(final int value, final String paramName) {
if (value < 0) {
throw new StepExecutionException(paramName + " parameter with value '" + value + "' must not be negative", this);
}
}
protected void integerParamCheck(final String param, final String paramName, final boolean nonNegative) {
try {
final int value = Integer.parseInt(param);
if (nonNegative && value < 0) {
throw new StepExecutionException(paramName + " parameter with value '" + param + "' must not be negative", this);
}
} catch (NumberFormatException e) {
throw new StepExecutionException("Can't parse " + paramName + " parameter with value '" + param + "' as an integer.", this);
}
}
protected void optionalIntegerParamCheck(final String param, final String paramName, final boolean nonNegative) {
if (!StringUtils.isEmpty(param)) {
integerParamCheck(param, paramName, nonNegative);
}
}
protected void nullResponseCheck() {
paramCheck(getContext() == null || getContext().getCurrentResponse() == null,
"No current response available! Is previous invoke missing?");
}
/**
* Gets a snapshot of the values.
* As the value of the attributes can change over time,
* it is not possible to fill and cache the Map.
* Either fill the Map everytime, or skip the fields and use only the Map.
* <p>This method returns all the parameters that were discovered at build time and stored in the <em>.attributes</em> resource.
* Overwrite this method if your step doesn't have a <em>.attributes</em> resource
*
* @return A Map of (attribute name, attribute value) for this step.
*/
public Map getParameterDictionary() {
final Map parameterDictionary = new TreeMap(); // to ensure order and make report comparison easier
addComputedParameters(parameterDictionary);
addInternalParameters(parameterDictionary);
return parameterDictionary;
}
/**
* Adds parameters that are not issued from the config file but computed at runtime by the step
*
* @param map the map in which the parameters should be added
*/
protected void addComputedParameters(final Map map) {
if (this instanceof IComputeValue) {
final String value = ((IComputeValue) this).getComputedValue();
MapUtil.putIfNotNull(map, "=> value", value);
}
}
/**
* TODO: would be cleaner to notify the result listener and to give him this information
*
* @return the "results" parameter of the step
*/
protected Map getComputedParameters() {
final Map map = new HashMap();
addComputedParameters(map);
return map;
}
private void addInternalParameters(final Map map) {
// add internal parameters
MapUtil.putIfNotNull(map, "taskName", getTaskName());
}
}