// Copyright � 2002-2005 Canoo Engineering AG, Switzerland.
package com.canoo.webtest.extension.applet;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import javax.xml.xpath.XPathExpressionException;
import junit.framework.TestCase;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Logger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.LogStreamHandler;
import org.apache.tools.ant.taskdefs.Taskdef;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.CommandlineJava;
import org.apache.tools.ant.types.Environment;
import org.apache.tools.ant.types.Path;
import org.netbeans.jemmy.Scenario;
import org.xml.sax.SAXException;
import com.canoo.webtest.ant.WebtestTask;
import com.canoo.webtest.boundary.AppletRunnerStepBoundary;
import com.canoo.webtest.boundary.UrlBoundary;
import com.canoo.webtest.engine.Context;
import com.canoo.webtest.engine.StepExecutionException;
import com.canoo.webtest.engine.StepFailedException;
import com.canoo.webtest.extension.applet.runner.AppletRunner;
import com.canoo.webtest.steps.request.AbstractTargetAction;
import com.canoo.webtest.steps.store.StoreCookie;
import com.canoo.webtest.util.FileUtil;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.WebRequestSettings;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
/**
* @author Denis N. Antonioli
* @webtest.step category="Extension"
* name="appletRunner"
* description="Runs and tests applets from inside webtest. If the applet triggers the load of a new page in the
* specified target, then this page becomes the current one."
*/
public class AppletRunnerStep extends AbstractTargetAction {
private static final Logger LOG = Logger.getLogger(AppletRunnerStep.class);
public static final String PROTOCOL_HANDLER_LIST = "java.protocol.handler.pkgs";
/**
* The package that is parent to all the necessary StreamHandler.
* We <em>do not</em> uses reflection to get this value,
* because we don't want any of these classes loaded in webtest jvm.
*/
public static final String RUNNER_PACKAGE = "com.canoo.webtest.extension.applet.runner";
/**
* The name of a class in the clover jar. Uses this constant to find the jar file on the classpath,
* if it is present.
*/
public static final String A_CLOVER_CLASS = "com.cenqua.clover.CloverInstr";
static final String LOG4J_DEFAULT_INIT_OVERRIDES = "log4j.defaultInitOverride";
static final String LOG4J_CONFIGURATION = "log4j.configuration";
static final Integer ZERO = new Integer(0);
private String fTarget;
private String fXPath;
private String fScenario;
private String fScenarioLocation;
private final Path fScenarioLocationPath = new Path(null);
private final ParameterSet fParameters = new ParameterSet();
private final CommandlineJava fCommandline = new CommandlineJava();
{
setupLog4j(fCommandline);
setupClasspath(fCommandline);
}
public String getScenarioLocation() {
return fScenarioLocation;
}
/**
* @param scenarioLocation
* @webtest.parameter required="no"
* description="The location of the scenario class."
*/
public void setScenarioLocation(String scenarioLocation) {
fScenarioLocation = scenarioLocation;
}
public String getScenario() {
return fScenario;
}
/**
* @param scenario
* @webtest.parameter required="yes"
* description="The name of the test scenario class."
*/
public void setScenario(String scenario) {
fScenario = scenario;
}
/**
* @param xpath
* @webtest.parameter required="yes"
* description="The XPath identifying the applet element to start."
*/
public void setXpath(String xpath) {
fXPath = xpath;
}
public String getXpath() {
return fXPath;
}
public String getTarget() {
return fTarget;
}
/**
* @param target
* @webtest.parameter required="no"
* description="A target to follow after the applet ends."
*/
public void setTarget(String target) {
fTarget = target;
}
public void setProject(Project p) {
super.setProject(p);
fScenarioLocationPath.setProject(getProject());
}
protected void verifyParameters() {
super.verifyParameters();
nullParamCheck(fXPath, "xpath");
nullParamCheck(fScenario, "scenario");
if (fScenarioLocation != null) {
fScenarioLocationPath.addExisting(new Path(getProject(), fScenarioLocation));
}
nullResponseCheck();
}
/**
* Adds the nested parameters children
*/
protected void addComputedParameters(final Map map)
{
for (final Iterator iterator = fParameters.iterator(); iterator.hasNext();) {
final Object obj = iterator.next();
if (obj instanceof Parameter) {
final Parameter param = (Parameter) obj;
map.put(param.getName(), param.getValue());
} else {
final ParameterRef paramRef = (ParameterRef) obj;
map.put(paramRef.getRegex(), paramRef.getPropertyType());
}
}
}
/**
* @deprecated Use {@link #addParameter(Parameter)}.
*/
public void addParam(final Parameter param) {
addParameter(param);
}
/**
* @param param
* @webtest.nested.parameter required="no"
* description="A parameter for the applet scenario."
*/
public void addParameter(final Parameter param) {
fParameters.add(param);
}
/**
* @param param
* @webtest.nested.parameter required="no"
* description="A parameter for the applet scenario."
*/
public void addParameterRef(final ParameterRef param) {
fParameters.add(param);
}
Iterator getParameters() {
return fParameters.iterator();
}
protected Page findTarget() throws IOException, SAXException, XPathExpressionException {
fParameters.expandProperties(this);
AppletPluginArguments appletPluginArguments = setUpAppletPluginArguments();
AppletPluginResults apr = runApplet(appletPluginArguments);
return findTargetWithResults(apr, getContext());
}
protected String getLogMessageForTarget() {
return "by applet showDocument";
}
protected Page findTargetWithResults(AppletPluginResults apr, Context context) throws IOException, SAXException {
if (fTarget != null) {
final Map allFrames = apr.getFrames();
if (allFrames.containsKey(fTarget)) {
final URL url = (URL) allFrames.get(fTarget);
return getResponse(new WebRequestSettings(url));
}
else {
for (final Iterator frames = allFrames.entrySet().iterator(); frames.hasNext();) {
final Map.Entry frame = (Map.Entry) frames.next();
LOG.error(frame.getKey() + " -> " + ((URL) frame.getValue()).toExternalForm());
}
throw new StepFailedException("The applet didn't showDocument in " + fTarget);
}
}
return null;
}
private AppletPluginResults readResults(AppletPluginArguments appletPluginArguments) {
return (AppletPluginResults) FileUtil.tryReadObjectFromFile(appletPluginArguments.getOutputFile(), this);
}
private AppletPluginArguments setUpAppletPluginArguments() throws XPathExpressionException {
final HtmlPage currentResponse = getContext().getCurrentHtmlResponse(this);
final HtmlElement appletNode = (HtmlElement) getContext().getXPathHelper().selectFirst(currentResponse, getXpath());
if (appletNode == null) {
throw new StepFailedException("The specified element <" + getXpath() + "> was not found.", this);
}
final AppletPluginArguments appletPluginArguments = createAppletPluginArguments();
appletPluginArguments.setAppletTag(extractAppletParameter(currentResponse.getWebResponse().getRequestUrl(), appletNode));
return appletPluginArguments;
}
AppletPluginArguments createAppletPluginArguments() {
final WebtestTask webtest = getContext().getWebtest();
final AppletPluginArguments appletPluginArguments = new AppletPluginArguments();
final StringBuffer sb = new StringBuffer(webtest.getName());
sb.append(getDescription(" - ", ""));
appletPluginArguments.setBaseWindowName(sb.toString());
appletPluginArguments.setSaveResponse(webtest.getConfig().isSaveResponse());
appletPluginArguments.setSaveDirectory(webtest.getConfig().getWebTestResultDir());
appletPluginArguments.setOutputFile(createTempFile(".output"));
appletPluginArguments.setScenarioLocation(convertPathToURL(fScenarioLocationPath));
appletPluginArguments.setScenario(fScenario);
for (Iterator iterator = getParameters(); iterator.hasNext();) {
appletPluginArguments.addArgument((Parameter) iterator.next());
}
appletPluginArguments.addCookies(StoreCookie.getCookies(getContext()));
return appletPluginArguments;
}
protected URL[] convertPathToURL(Path path) {
if (path == null) {
return AppletPluginArguments.EMPTY_URL_LIST;
}
String pathElements[] = path.list();
final URL[] urls = new URL[pathElements.length];
for (int i = 0; i < pathElements.length; i++) {
urls[i] = UrlBoundary.tryCreateUrlFromFileWithError(new File(pathElements[i]), this);
}
return urls;
}
AbstractAppletTag extractAppletParameter(final URL url, final HtmlElement appletNode) {
try {
return AbstractAppletTag.newInstance(url, appletNode);
} catch (Exception e) {
throw new StepFailedException(e.getMessage(), this);
}
}
private AppletPluginResults runApplet(AppletPluginArguments appletPluginArguments) {
int exitValue = executeAsForked(appletPluginArguments);
LOG.info("runApplet: exitValue was: " + exitValue);
if (exitValue != 0) {
final StringBuffer msg = new StringBuffer();
msg.append("Test ").append(fScenario).append(" failed. Exit value: ").append(exitValue);
final String s = msg.toString();
LOG.error(s);
throw new StepExecutionException(s, this);
}
AppletPluginResults apr = readResults(appletPluginArguments);
verifyAppletResult(apr);
for (Iterator properties = apr.getProperties().iterator(); properties.hasNext();) {
AppletPluginResults.Property property = (AppletPluginResults.Property) properties.next();
setWebtestProperty(property.getName(), property.getValue(), property.getType());
}
return apr;
}
void verifyAppletResult(AppletPluginResults apr) {
if (apr.getException() != null) {
throw new StepFailedException(apr.getException(), this);
}
if (apr.getJemmyException() != null) {
throw new StepFailedException(apr.getJemmyException(), this);
}
if (!ZERO.equals(apr.getReturnValue())) {
throw new StepFailedException("Scenario returns " + apr.getReturnValue(), this);
}
}
/**
* Execute a testcase by forking a new JVM. The command will block until it finishes.
*
* @param appletPluginArguments
*/
private int executeAsForked(final AppletPluginArguments appletPluginArguments) {
CommandlineJava cmd = AppletRunnerStepBoundary.tryClone(fCommandline, this);
cmd.setClassname(AppletRunner.class.getName());
cmd.addSysproperty(getProtocolHandler());
cmd.createArgument().setValue(writeArguments(appletPluginArguments));
Execute execute = new Execute(new LogStreamHandler(this, Project.MSG_INFO, Project.MSG_WARN));
execute.setCommandline(cmd.getCommandline());
execute.setAntRun(getProject());
// propagate the environment here?
LOG.info(cmd.describeCommand());
return AppletRunnerStepBoundary.tryExecute(execute, this);
}
static Environment.Variable getProtocolHandler() {
final Environment.Variable sysp = new Environment.Variable();
sysp.setKey(PROTOCOL_HANDLER_LIST);
final String handlers = System.getProperty(PROTOCOL_HANDLER_LIST, "");
sysp.setValue(RUNNER_PACKAGE + (handlers.length() == 0 ? "" : "|" + handlers));
return sysp;
}
/**
* Adds all the jar required to start the appletrunner in a separate vm.
*
* @param cmd
*/
private void setupClasspath(CommandlineJava cmd) {
final Path classpath = cmd.createClasspath(getProject());
appendToClasspath(LogFactory.class, classpath);
appendToClasspath(HostConfiguration.class, classpath);
appendToClasspath(Logger.class, classpath);
appendToClasspath(Commandline.Argument.class, classpath);
appendToClasspath(HtmlElement.class, classpath);
appendToClasspath(DecoderException.class, classpath);
appendOptionalToClasspath(A_CLOVER_CLASS, classpath);
appendToClasspath(Scenario.class, classpath);
appendToClasspath(AppletRunner.class, classpath);
appendToClasspath(TestCase.class, classpath);
}
/**
* Settings for log4j in the child jvm. The logic here is derived from the description in log4j's short manual.
*
* @param cmd The command line for the child jvm.
*/
static void setupLog4j(CommandlineJava cmd) {
String log4jDefaultInitOverride = System.getProperty(LOG4J_DEFAULT_INIT_OVERRIDES, "false");
if ("false".equals(log4jDefaultInitOverride)) {
String resource = System.getProperty(LOG4J_CONFIGURATION, "log4j.properties");
URL resourceURL;
try {
resourceURL = new URL(resource);
} catch (MalformedURLException e) {
resourceURL = org.apache.log4j.helpers.Loader.getResource(resource);
}
if (resourceURL == null) {
log4jDefaultInitOverride = "true";
} else {
setSysProperty(cmd, LOG4J_CONFIGURATION, resourceURL.toExternalForm());
}
}
setSysProperty(cmd, LOG4J_DEFAULT_INIT_OVERRIDES, log4jDefaultInitOverride);
}
static void setSysProperty(final CommandlineJava cmd, final String key, final String value) {
final Environment.Variable sysp = new Environment.Variable();
sysp.setKey(key);
sysp.setValue(value);
cmd.addSysproperty(sysp);
}
/**
* Adds a jar file for a given class to the classpath.
*
* @param aClass The class to look for.
* @param classpath The claspath to change.
*/
void appendToClasspath(Class aClass, final Path classpath) {
if (!appendOptionalToClasspath(aClass.getName(), classpath)) {
String msg = "Can't locate required class " + aClass.getName();
LOG.error(msg);
throw new StepFailedException(msg, this);
}
}
/**
* Adds a jar file for a given class to the classpath.
*
* @param aClassName The class to look for.
* @param classpath The claspath to change.
* @return True if the desired class was added to the classpath.
*/
boolean appendOptionalToClasspath(String aClassName, final Path classpath) {
URL url = AppletRunnerStepBoundary.tryGetUrlForClass(aClassName, this);
if (url == null) {
LOG.warn("Can't locate optional class " + aClassName);
return false;
}
classpath.createPathElement().setLocation(new File(url.getFile()));
return true;
}
private String writeArguments(AppletPluginArguments apa) {
final File tmpFile = createTempFile(".arguments");
FileUtil.tryWriteObjectToFile(tmpFile, apa, this);
return tmpFile.getAbsolutePath();
}
private File createTempFile(final String suffix) {
return FileUtil.tryCreateTempFile("AppletPlugin", suffix, this);
}
public static URL getUrlForClass(final String className) throws MalformedURLException {
LOG.debug("Looking for " + className);
final String classRsrc = className.replace('.', '/') + ".class";
final URL resource = AppletRunnerStep.class.getClassLoader().getResource(classRsrc);
if (resource == null) {
return null;
}
return new URL(resource, extractClasspathEntry(resource.getFile(), classRsrc));
}
/**
* Cuts off the name of the class and the '!/' that indicates a url into a jar.
*
* @param url
* @param classRsrc
* @return the class name
*/
static String extractClasspathEntry(final String url, final String classRsrc) {
int length = url.length() - classRsrc.length();
if ("!/".equals(url.substring(length - 2, length))) {
length -= 2;
}
return url.substring(0, length);
}
/**
* Adds a JVM argument.
*
* @return create a new JVM argument so that any argument can be passed to the JVM.
* @since Ant 1.2
*/
public Commandline.Argument createJvmarg() {
return fCommandline.createVmArgument();
}
}