001: // Copyright © 2002-2005 Canoo Engineering AG, Switzerland.
002: package com.canoo.webtest.extension.applet;
003:
004: import java.io.File;
005: import java.io.IOException;
006: import java.net.MalformedURLException;
007: import java.net.URL;
008: import java.util.Iterator;
009: import java.util.Map;
010:
011: import org.apache.commons.codec.DecoderException;
012: import org.apache.commons.httpclient.HostConfiguration;
013: import org.apache.commons.logging.LogFactory;
014: import org.apache.log4j.Logger;
015: import org.apache.tools.ant.Project;
016: import org.apache.tools.ant.taskdefs.Execute;
017: import org.apache.tools.ant.taskdefs.LogStreamHandler;
018: import org.apache.tools.ant.types.Commandline;
019: import org.apache.tools.ant.types.CommandlineJava;
020: import org.apache.tools.ant.types.Environment;
021: import org.apache.tools.ant.types.Path;
022: import org.jaxen.JaxenException;
023: import org.jaxen.XPath;
024: import org.netbeans.jemmy.Scenario;
025: import org.xml.sax.SAXException;
026:
027: import com.canoo.webtest.ant.WebtestTask;
028: import com.canoo.webtest.boundary.AppletRunnerStepBoundary;
029: import com.canoo.webtest.boundary.UrlBoundary;
030: import com.canoo.webtest.engine.Context;
031: import com.canoo.webtest.engine.StepExecutionException;
032: import com.canoo.webtest.engine.StepFailedException;
033: import com.canoo.webtest.extension.applet.runner.AppletRunner;
034: import com.canoo.webtest.steps.request.AbstractTargetAction;
035: import com.canoo.webtest.steps.store.StoreCookie;
036: import com.canoo.webtest.util.FileUtil;
037: import com.gargoylesoftware.htmlunit.Page;
038: import com.gargoylesoftware.htmlunit.WebRequestSettings;
039: import com.gargoylesoftware.htmlunit.html.HtmlElement;
040: import com.gargoylesoftware.htmlunit.html.HtmlPage;
041:
042: /**
043: * @author Denis N. Antonioli
044: * @webtest.step category="Extension"
045: * name="appletRunner"
046: * description="Runs and tests applets from inside webtest. If the applet triggers the load of a new page in the
047: * specified target, then this page becomes the current one."
048: */
049: public class AppletRunnerStep extends AbstractTargetAction {
050: private static final Logger LOG = Logger
051: .getLogger(AppletRunnerStep.class);
052: public static final String PROTOCOL_HANDLER_LIST = "java.protocol.handler.pkgs";
053: /**
054: * The package that is parent to all the necessary StreamHandler.
055: * We <em>do not</em> uses reflection to get this value,
056: * because we don't want any of these classes loaded in webtest jvm.
057: */
058: public static final String RUNNER_PACKAGE = "com.canoo.webtest.extension.applet.runner";
059: /**
060: * The name of a class in the clover jar. Uses this constant to find the jar file on the classpath,
061: * if it is present.
062: */
063: public static final String A_CLOVER_CLASS = "com.cenqua.clover.CloverInstr";
064: static final String LOG4J_DEFAULT_INIT_OVERRIDES = "log4j.defaultInitOverride";
065: static final String LOG4J_CONFIGURATION = "log4j.configuration";
066: static final Integer ZERO = new Integer(0);
067:
068: private String fTarget;
069: private String fXPath;
070: private String fScenario;
071: private String fScenarioLocation;
072: private final Path fScenarioLocationPath = new Path(null);
073: private final ParameterSet fParameters = new ParameterSet();
074: private final CommandlineJava fCommandline = new CommandlineJava();
075:
076: {
077: setupLog4j(fCommandline);
078: setupClasspath(fCommandline);
079: }
080:
081: public String getScenarioLocation() {
082: return fScenarioLocation;
083: }
084:
085: /**
086: * @param scenarioLocation
087: * @webtest.parameter required="no"
088: * description="The location of the scenario class."
089: */
090: public void setScenarioLocation(String scenarioLocation) {
091: fScenarioLocation = scenarioLocation;
092: }
093:
094: public String getScenario() {
095: return fScenario;
096: }
097:
098: /**
099: * @param scenario
100: * @webtest.parameter required="yes"
101: * description="The name of the test scenario class."
102: */
103: public void setScenario(String scenario) {
104: fScenario = scenario;
105: }
106:
107: /**
108: * @param xpath
109: * @webtest.parameter required="yes"
110: * description="The XPath identifying the applet element to start."
111: */
112: public void setXpath(String xpath) {
113: fXPath = xpath;
114: }
115:
116: public String getXpath() {
117: return fXPath;
118: }
119:
120: public String getTarget() {
121: return fTarget;
122: }
123:
124: /**
125: * @param target
126: * @webtest.parameter required="no"
127: * description="A target to follow after the applet ends."
128: */
129: public void setTarget(String target) {
130: fTarget = target;
131: }
132:
133: public void setProject(Project p) {
134: super .setProject(p);
135: fScenarioLocationPath.setProject(getProject());
136: }
137:
138: protected void verifyParameters() {
139: super .verifyParameters();
140: nullParamCheck(fXPath, "xpath");
141: nullParamCheck(fScenario, "scenario");
142: if (fScenarioLocation != null) {
143: fScenarioLocationPath.addExisting(new Path(getProject(),
144: fScenarioLocation));
145: }
146: nullResponseCheck();
147: }
148:
149: /**
150: * Adds the nested parameters children
151: */
152: protected void addComputedParameters(final Map map) {
153: for (final Iterator iterator = fParameters.iterator(); iterator
154: .hasNext();) {
155: final Object obj = iterator.next();
156: if (obj instanceof Parameter) {
157: final Parameter param = (Parameter) obj;
158: map.put(param.getName(), param.getValue());
159: } else {
160: final ParameterRef paramRef = (ParameterRef) obj;
161: map
162: .put(paramRef.getRegex(), paramRef
163: .getPropertyType());
164: }
165: }
166: }
167:
168: /**
169: * @deprecated Use {@link #addParameter(Parameter)}.
170: */
171: public void addParam(final Parameter param) {
172: addParameter(param);
173: }
174:
175: /**
176: * @param param
177: * @webtest.nested.parameter required="no"
178: * description="A parameter for the applet scenario."
179: */
180: public void addParameter(final Parameter param) {
181: fParameters.add(param);
182: }
183:
184: /**
185: * @param param
186: * @webtest.nested.parameter required="no"
187: * description="A parameter for the applet scenario."
188: */
189: public void addParameterRef(final ParameterRef param) {
190: fParameters.add(param);
191: }
192:
193: Iterator getParameters() {
194: return fParameters.iterator();
195: }
196:
197: protected Page findTarget() throws JaxenException, IOException,
198: SAXException {
199: fParameters.expandProperties(this );
200:
201: AppletPluginArguments appletPluginArguments = setUpAppletPluginArguments();
202: AppletPluginResults apr = runApplet(appletPluginArguments);
203: return findTargetWithResults(apr, getContext());
204: }
205:
206: protected String getLogMessageForTarget() {
207: return "by applet showDocument";
208: }
209:
210: protected Page findTargetWithResults(AppletPluginResults apr,
211: Context context) throws IOException, SAXException {
212: if (fTarget != null) {
213: final Map allFrames = apr.getFrames();
214: if (allFrames.containsKey(fTarget)) {
215: final URL url = (URL) allFrames.get(fTarget);
216: return getResponse(new WebRequestSettings(url));
217: } else {
218: for (final Iterator frames = allFrames.entrySet()
219: .iterator(); frames.hasNext();) {
220: final Map.Entry frame = (Map.Entry) frames.next();
221: LOG
222: .error(frame.getKey()
223: + " -> "
224: + ((URL) frame.getValue())
225: .toExternalForm());
226: }
227: throw new StepFailedException(
228: "The applet didn't showDocument in " + fTarget);
229: }
230: }
231: return null;
232: }
233:
234: private AppletPluginResults readResults(
235: AppletPluginArguments appletPluginArguments) {
236: return (AppletPluginResults) FileUtil.tryReadObjectFromFile(
237: appletPluginArguments.getOutputFile(), this );
238: }
239:
240: private AppletPluginArguments setUpAppletPluginArguments() {
241: final HtmlElement appletNode;
242: final HtmlPage currentResponse = getContext()
243: .getCurrentHtmlResponse(this );
244: try {
245: final XPath xpath = getContext().getXPathHelper().getXPath(
246: currentResponse, getXpath());
247: appletNode = (HtmlElement) xpath
248: .selectSingleNode(currentResponse);
249: } catch (JaxenException e) {
250: final String msg = "Xpath error for '" + getXpath() + "'";
251: LOG.error(msg, e);
252: throw new StepExecutionException(
253: msg + " " + e.getMessage(), this );
254: }
255: if (appletNode == null) {
256: throw new StepFailedException("The specified element <"
257: + getXpath() + "> was not found.", this );
258: }
259:
260: final AppletPluginArguments appletPluginArguments = createAppletPluginArguments();
261: appletPluginArguments.setAppletTag(extractAppletParameter(
262: currentResponse.getWebResponse().getUrl(), appletNode));
263: return appletPluginArguments;
264: }
265:
266: AppletPluginArguments createAppletPluginArguments() {
267: final WebtestTask webtest = getContext().getWebtest();
268: final AppletPluginArguments appletPluginArguments = new AppletPluginArguments();
269:
270: final StringBuffer sb = new StringBuffer(webtest.getName());
271: sb.append(getDescription(" - ", ""));
272: appletPluginArguments.setBaseWindowName(sb.toString());
273:
274: appletPluginArguments.setSaveResponse(webtest.getConfig()
275: .isSaveResponse());
276: appletPluginArguments.setSaveDirectory(webtest.getConfig()
277: .getWebTestResultDir());
278: appletPluginArguments.setOutputFile(createTempFile(".output"));
279: appletPluginArguments
280: .setScenarioLocation(convertPathToURL(fScenarioLocationPath));
281: appletPluginArguments.setScenario(fScenario);
282: for (Iterator iterator = getParameters(); iterator.hasNext();) {
283: appletPluginArguments.addArgument((Parameter) iterator
284: .next());
285: }
286: appletPluginArguments.addCookies(StoreCookie
287: .getCookies(getContext()));
288: return appletPluginArguments;
289: }
290:
291: protected URL[] convertPathToURL(Path path) {
292: if (path == null) {
293: return AppletPluginArguments.EMPTY_URL_LIST;
294: }
295: String pathElements[] = path.list();
296: final URL[] urls = new URL[pathElements.length];
297: for (int i = 0; i < pathElements.length; i++) {
298: urls[i] = UrlBoundary.tryCreateUrlFromFileWithError(
299: new File(pathElements[i]), this );
300: }
301: return urls;
302: }
303:
304: AbstractAppletTag extractAppletParameter(final URL url,
305: final HtmlElement appletNode) {
306: try {
307: return AbstractAppletTag.newInstance(url, appletNode);
308: } catch (Exception e) {
309: throw new StepFailedException(e.getMessage(), this );
310: }
311: }
312:
313: private AppletPluginResults runApplet(
314: AppletPluginArguments appletPluginArguments) {
315: int exitValue = executeAsForked(appletPluginArguments);
316: LOG.info("runApplet: exitValue was: " + exitValue);
317: if (exitValue != 0) {
318: final StringBuffer msg = new StringBuffer();
319: msg.append("Test ").append(fScenario).append(
320: " failed. Exit value: ").append(exitValue);
321:
322: final String s = msg.toString();
323: LOG.error(s);
324: throw new StepExecutionException(s, this );
325: }
326: AppletPluginResults apr = readResults(appletPluginArguments);
327:
328: verifyAppletResult(apr);
329:
330: for (Iterator properties = apr.getProperties().iterator(); properties
331: .hasNext();) {
332: AppletPluginResults.Property property = (AppletPluginResults.Property) properties
333: .next();
334: setWebtestProperty(property.getName(), property.getValue(),
335: property.getType());
336: }
337:
338: return apr;
339: }
340:
341: void verifyAppletResult(AppletPluginResults apr) {
342: if (apr.getException() != null) {
343: throw new StepFailedException(apr.getException(), this );
344: }
345: if (apr.getJemmyException() != null) {
346: throw new StepFailedException(apr.getJemmyException(), this );
347: }
348: if (!ZERO.equals(apr.getReturnValue())) {
349: throw new StepFailedException("Scenario returns "
350: + apr.getReturnValue(), this );
351: }
352: }
353:
354: /**
355: * Execute a testcase by forking a new JVM. The command will block until it finishes.
356: *
357: * @param appletPluginArguments
358: */
359: private int executeAsForked(
360: final AppletPluginArguments appletPluginArguments) {
361: CommandlineJava cmd = AppletRunnerStepBoundary.tryClone(
362: fCommandline, this );
363: cmd.setClassname(AppletRunner.class.getName());
364: cmd.addSysproperty(getProtocolHandler());
365: cmd.createArgument().setValue(
366: writeArguments(appletPluginArguments));
367: Execute execute = new Execute(new LogStreamHandler(this ,
368: Project.MSG_INFO, Project.MSG_WARN));
369: execute.setCommandline(cmd.getCommandline());
370: execute.setAntRun(getProject());
371:
372: // propagate the environment here?
373: LOG.info(cmd.describeCommand());
374: return AppletRunnerStepBoundary.tryExecute(execute, this );
375: }
376:
377: static Environment.Variable getProtocolHandler() {
378: final Environment.Variable sysp = new Environment.Variable();
379: sysp.setKey(PROTOCOL_HANDLER_LIST);
380: final String handlers = System.getProperty(
381: PROTOCOL_HANDLER_LIST, "");
382: sysp.setValue(RUNNER_PACKAGE
383: + (handlers.length() == 0 ? "" : "|" + handlers));
384: return sysp;
385: }
386:
387: /**
388: * Adds all the jar required to start the appletrunner in a separate vm.
389: *
390: * @param cmd
391: */
392: private void setupClasspath(CommandlineJava cmd) {
393: final Path classpath = cmd.createClasspath(getProject());
394: appendToClasspath(LogFactory.class, classpath);
395: appendToClasspath(HostConfiguration.class, classpath);
396: appendToClasspath(Logger.class, classpath);
397: appendToClasspath(Commandline.Argument.class, classpath);
398: appendToClasspath(HtmlElement.class, classpath);
399: appendToClasspath(DecoderException.class, classpath);
400: appendOptionalToClasspath(A_CLOVER_CLASS, classpath);
401: appendToClasspath(Scenario.class, classpath);
402: appendToClasspath(AppletRunner.class, classpath);
403: }
404:
405: /**
406: * Settings for log4j in the child jvm. The logic here is derived from the description in log4j's short manual.
407: *
408: * @param cmd The command line for the child jvm.
409: */
410: static void setupLog4j(CommandlineJava cmd) {
411: String log4jDefaultInitOverride = System.getProperty(
412: LOG4J_DEFAULT_INIT_OVERRIDES, "false");
413: if ("false".equals(log4jDefaultInitOverride)) {
414: String resource = System.getProperty(LOG4J_CONFIGURATION,
415: "log4j.properties");
416: URL resourceURL;
417: try {
418: resourceURL = new URL(resource);
419: } catch (MalformedURLException e) {
420: resourceURL = org.apache.log4j.helpers.Loader
421: .getResource(resource);
422: }
423: if (resourceURL == null) {
424: log4jDefaultInitOverride = "true";
425: } else {
426: setSysProperty(cmd, LOG4J_CONFIGURATION, resourceURL
427: .toExternalForm());
428: }
429: }
430: setSysProperty(cmd, LOG4J_DEFAULT_INIT_OVERRIDES,
431: log4jDefaultInitOverride);
432: }
433:
434: static void setSysProperty(final CommandlineJava cmd,
435: final String key, final String value) {
436: final Environment.Variable sysp = new Environment.Variable();
437: sysp.setKey(key);
438: sysp.setValue(value);
439: cmd.addSysproperty(sysp);
440: }
441:
442: /**
443: * Adds a jar file for a given class to the classpath.
444: *
445: * @param aClass The class to look for.
446: * @param classpath The claspath to change.
447: */
448: void appendToClasspath(Class aClass, final Path classpath) {
449: if (!appendOptionalToClasspath(aClass.getName(), classpath)) {
450: String msg = "Can't locate required class "
451: + aClass.getName();
452: LOG.error(msg);
453: throw new StepFailedException(msg, this );
454: }
455: }
456:
457: /**
458: * Adds a jar file for a given class to the classpath.
459: *
460: * @param aClassName The class to look for.
461: * @param classpath The claspath to change.
462: * @return True if the desired class was added to the classpath.
463: */
464: boolean appendOptionalToClasspath(String aClassName,
465: final Path classpath) {
466: URL url = AppletRunnerStepBoundary.tryGetUrlForClass(
467: aClassName, this );
468: if (url == null) {
469: LOG.warn("Can't locate optional class " + aClassName);
470: return false;
471: }
472: classpath.createPathElement().setLocation(
473: new File(url.getFile()));
474: return true;
475: }
476:
477: private String writeArguments(AppletPluginArguments apa) {
478: final File tmpFile = createTempFile(".arguments");
479: FileUtil.tryWriteObjectToFile(tmpFile, apa, this );
480: return tmpFile.getAbsolutePath();
481: }
482:
483: private File createTempFile(final String suffix) {
484: return FileUtil.tryCreateTempFile("AppletPlugin", suffix, this );
485: }
486:
487: public static URL getUrlForClass(final String className)
488: throws MalformedURLException {
489: LOG.debug("Looking for " + className);
490: final String classRsrc = className.replace('.', '/') + ".class";
491: final URL resource = AppletRunnerStep.class.getClassLoader()
492: .getResource(classRsrc);
493: if (resource == null) {
494: return null;
495: }
496: return new URL(resource, extractClasspathEntry(resource
497: .getFile(), classRsrc));
498: }
499:
500: /**
501: * Cuts off the name of the class and the '!/' that indicates a url into a jar.
502: *
503: * @param url
504: * @param classRsrc
505: * @return the class name
506: */
507: static String extractClasspathEntry(final String url,
508: final String classRsrc) {
509: int length = url.length() - classRsrc.length();
510: if ("!/".equals(url.substring(length - 2, length))) {
511: length -= 2;
512: }
513: return url.substring(0, length);
514: }
515:
516: /**
517: * Adds a JVM argument.
518: *
519: * @return create a new JVM argument so that any argument can be passed to the JVM.
520: * @since Ant 1.2
521: */
522: public Commandline.Argument createJvmarg() {
523: return fCommandline.createVmArgument();
524: }
525: }
|