001: // Copyright © 2002-2007 Canoo Engineering AG, Switzerland.
002: package com.canoo.webtest.steps;
003:
004: import java.io.Serializable;
005: import java.util.Date;
006: import java.util.HashMap;
007: import java.util.Iterator;
008: import java.util.Map;
009: import java.util.TreeMap;
010:
011: import org.apache.commons.lang.ClassUtils;
012: import org.apache.commons.lang.StringUtils;
013: import org.apache.log4j.Logger;
014: import org.apache.tools.ant.BuildListener;
015: import org.apache.tools.ant.Task;
016:
017: import com.canoo.webtest.ant.WebtestTask;
018: import com.canoo.webtest.engine.Context;
019: import com.canoo.webtest.engine.EqualsStringVerfier;
020: import com.canoo.webtest.engine.IStringVerifier;
021: import com.canoo.webtest.engine.RegExStringVerifier;
022: import com.canoo.webtest.engine.StepExecutionException;
023: import com.canoo.webtest.interfaces.IComputeValue;
024: import com.canoo.webtest.reporting.IStepResultListener;
025: import com.canoo.webtest.util.Checker;
026: import com.canoo.webtest.util.MapUtil;
027: import com.gargoylesoftware.htmlunit.Page;
028:
029: /**
030: * Abstract superclass for all test steps. Provides generic services for all subclasses.
031: *
032: * @author Carsten Seibert
033: * @author Marc Guillemot
034: * @author Paul King, ASERT
035: * @webtest.step
036: */
037: public abstract class Step extends Task implements Serializable,
038: Cloneable {
039: private static final Logger LOG = Logger.getLogger(Step.class);
040: public static final String ELEMENT_ATTRIBUTE_NAME = "name";
041: public static final String ELEMENT_ATTRIBUTE_ID = "id";
042: /**
043: * @deprecated The default is now not to set the value at all.
044: */
045: public static final String DEFAULT_DESCRIPTION = "<unknown>";
046:
047: private Date fStarted;
048: private Date fCompleted;
049: private boolean fSuccessful;
050:
051: /**
052: * The property type is set according to webtest's <em>defaultPropertyType</em>.
053: */
054: public static final String PROPERTY_TYPE_DEFAULT = null;
055: /**
056: * The property is a <em>dynamic</em> property.
057: */
058: public static final String PROPERTY_TYPE_DYNAMIC = "dynamic";
059: /**
060: * The property is an <em>ant</em> property.
061: */
062: public static final String PROPERTY_TYPE_ANT = "ant";
063: /**
064: * The property is an <em>ant</em> property that must not overwrite an existing property.
065: */
066: public static final String PROPERTY_TYPE_ANT_STRICT = "antstrict";
067:
068: /**
069: * This is the abstract base class for all test step specifications.
070: */
071: protected Step() {
072: }
073:
074: private void checkSetup() {
075: Checker.assertNonNull(getProject(), "Project cannot be null");
076: }
077:
078: public Context getContext() {
079: return WebtestTask.getThreadContext();
080: }
081:
082: /**
083: * Called to perform the step's functionality.
084: * Before calling this method, the step has been completely initialized (including expanding and verifying the parameters)
085: * and the environment notified of the start.
086: *
087: * @throws Exception
088: */
089: public abstract void doExecute() throws Exception;
090:
091: /**
092: * Ant calls this method to invoke this task's functionality.
093: * We expand and verify the steps parameters then call doExecute()
094: * as well as handle lifecycle notifications.
095: */
096: public void execute() {
097: //expandProperties(); FIXME: use PropertyHelper!
098: checkContextDefined();
099: notifyStarted();
100: try {
101: verifyParameters();
102: doExecute();
103:
104: // give time for background js tasks to finish when needed
105: final Page currentPage = getContext().getCurrentResponse();
106: if (currentPage != null
107: && getContext().getConfig().isEasyAjax())
108: currentPage.getEnclosingWindow().getThreadManager()
109: .joinAll(2000);
110: } catch (final Exception ex) {
111: handleException(ex);
112: } finally {
113: notifyCompleted();
114: notifyStepResultsListeners();
115: }
116: notifySuccess();
117: }
118:
119: private void checkContextDefined() {
120: if (WebtestTask.getThreadContext() == null) {
121: throw new StepExecutionException(
122: "Step not inside a webtest", this );
123: }
124: }
125:
126: /**
127: * Notifies the interested project build listeners (typically only the {@link com.canoo.webtest.reporting.StepExecutionListener})
128: * that this step has produced results
129: */
130: protected void notifyStepResultsListeners() {
131: final Map results = getComputedParameters();
132: if (results.isEmpty()) {
133: LOG
134: .debug("Step didn't produce results, no need to notifying listeners");
135: return;
136: }
137:
138: for (final Iterator iter = getProject().getBuildListeners()
139: .iterator(); iter.hasNext();) {
140: final BuildListener listener = (BuildListener) iter.next();
141: if (listener instanceof IStepResultListener) {
142: LOG.debug("Notifying " + listener + " of "
143: + results.size() + " results");
144: ((IStepResultListener) listener).stepResults(results);
145: }
146: }
147: }
148:
149: /**
150: * Called to let the step's implementation validate its parameters.
151: * The method is called after parameter extensions but before {@link #doExecute()}.
152: * This implementation does nothing, overwrite as needed.
153: */
154: protected void verifyParameters() {
155: // default is do nothing
156: }
157:
158: public boolean hasDescription() {
159: return StringUtils.isNotEmpty(getDescription());
160: }
161:
162: /**
163: * Gets the description with a prefix and suffix if the description is set.
164: *
165: * @param prefix A string to display before the description.
166: * @param suffix A string to display after the description.
167: * @return the concatenation of prefix, description and suffix.
168: */
169: public String getDescription(final String prefix,
170: final String suffix) {
171: final String description = getDescription();
172: if (!StringUtils.isNotEmpty(description)) {
173: return "";
174: }
175: return prefix + description + suffix;
176: }
177:
178: /**
179: * Gets the execution time for a completed step.
180: *
181: * @return the execution time in ms
182: */
183: public long getDuration() {
184: return fCompleted.getTime() - fStarted.getTime();
185: }
186:
187: protected String getStepLabel() {
188: return "Step[" + getStepLabelBrief() + "]";
189: }
190:
191: private String getStepLabelBrief() {
192: final StringBuffer message = new StringBuffer();
193: message.append(getTaskName());
194: message.append(getDescription(" \"", "\""));
195: message.append(" (")
196: .append(getContext().getCurrentStepNumber())
197: .append("/");
198: message.append(getContext().getNumberOfSteps()).append(")");
199:
200: return message.toString();
201: }
202:
203: public boolean isCompleted() {
204: return fStarted != null && fCompleted != null;
205: }
206:
207: public boolean isStarted() {
208: return fStarted != null;
209: }
210:
211: public boolean isSuccessful() {
212: return fSuccessful;
213: }
214:
215: /**
216: * Called after {@link #doExecute()} has completed (successfully or not)
217: */
218: public void notifyCompleted() {
219: fCompleted = new Date();
220: LOG.debug("Completed Step: " + getStepLabelBrief());
221: }
222:
223: /**
224: * Called before calling {@link #doExecute()}
225: */
226: public void notifyStarted() {
227: fStarted = new Date();
228: LOG.info(">>>> Start Step: " + getStepLabelBrief());
229: }
230:
231: public void notifySuccess() {
232: fSuccessful = true;
233: LOG.debug("<<<< Successful Step: " + getStepLabelBrief());
234: }
235:
236: /**
237: * @param description
238: * @deprecated since June 10 2005. Use {@link Task#setDescription(String)}
239: * (setter should not be removed for compatibility with existing test sequences)
240: */
241: public void setStepid(final String description) {
242: LOG.warn("'stepid' is deprecated - use 'description' instead");
243: setDescription(description);
244: }
245:
246: public String toString() {
247: final StringBuffer sb = new StringBuffer(64);
248:
249: sb.append(ClassUtils.getShortClassName(getClass()));
250: sb.append(" at ");
251: sb.append(getLocation().toString());
252: sb.append(" with (");
253:
254: final Map parms = getParameterDictionary();
255:
256: for (final Iterator iter = parms.keySet().iterator(); iter
257: .hasNext(); sb.append(", ")) {
258: final Object param = iter.next();
259: sb.append(param).append("=\"").append(parms.get(param))
260: .append("\"");
261: }
262: if (!parms.isEmpty()) {
263: sb.setLength(sb.length() - 2);
264: }
265: sb.append(")");
266: return sb.toString();
267: }
268:
269: protected String getDefaultPropertyType() {
270: return getContext().getConfig().getDefaultPropertyType();
271: }
272:
273: /**
274: * Sets a property of the default type.
275: *
276: * @param name The name of the property.
277: * @param value The value of the property.
278: */
279: public void setWebtestProperty(final String name, final String value) {
280: setWebtestProperty(name, value, null);
281: }
282:
283: /**
284: * Sets a property of the default type.
285: *
286: * @param name The name of the property.
287: * @param value The value of the property.
288: * @param propertyType The kind of property desired. One of {@link #PROPERTY_TYPE_ANT},
289: * {@link #PROPERTY_TYPE_ANT_STRICT}, {@link #PROPERTY_TYPE_DYNAMIC} or {@link #PROPERTY_TYPE_DEFAULT}.
290: */
291: public void setWebtestProperty(final String name,
292: final String value, final String propertyType) {
293: final String this PropType = propertyType == PROPERTY_TYPE_DEFAULT ? getDefaultPropertyType()
294: : propertyType;
295:
296: LOG.debug("setWebtestProperty: " + name + "=" + value + " ["
297: + this PropType + "]");
298: if (StringUtils.isEmpty(this PropType)
299: || PROPERTY_TYPE_DYNAMIC.equals(this PropType)) {
300: getContext().getWebtest().setDynamicProperty(name, value);
301: return;
302: }
303:
304: if (PROPERTY_TYPE_ANT.equals(this PropType)) {
305: checkSetup();
306: getProject().setProperty(name, value);
307: return;
308: }
309:
310: if (PROPERTY_TYPE_ANT_STRICT.equals(this PropType)) {
311: checkSetup();
312: getProject().setNewProperty(name, value);
313: return;
314: }
315:
316: throw new StepExecutionException("Unknown propertyType: "
317: + this PropType, this );
318: }
319:
320: /**
321: * Gets a property of the default type.
322: *
323: * @param name The name of the property.
324: * @return The value of the property.
325: */
326: public String getWebtestProperty(final String name) {
327: return getWebtestProperty(name, PROPERTY_TYPE_DEFAULT);
328: }
329:
330: /**
331: * Gets a property of the specified type
332: *
333: * @param name The name of the property.
334: * @param propertyType The kind of property desired. One of {@link #PROPERTY_TYPE_ANT},
335: * {@link #PROPERTY_TYPE_DYNAMIC} or {@link #PROPERTY_TYPE_DEFAULT}.
336: * @return The value of the property.
337: */
338: public String getWebtestProperty(final String name,
339: final String propertyType) {
340: final String this PropType = propertyType == PROPERTY_TYPE_DEFAULT ? getDefaultPropertyType()
341: : propertyType;
342:
343: LOG.debug("getWebtestProperty(" + name + ") [" + this PropType
344: + "]");
345: if (StringUtils.isEmpty(this PropType)
346: || PROPERTY_TYPE_DYNAMIC.equals(this PropType)) {
347: return getContext().getWebtest().getDynamicProperty(name);
348: }
349:
350: if (this PropType.startsWith(PROPERTY_TYPE_ANT)) {
351: checkSetup();
352: return getProject().getProperty(name);
353: }
354: throw new StepExecutionException("Unknown propertyType: "
355: + this PropType, this );
356: }
357:
358: public Map getWebtestProperties() {
359: return getWebtestProperties(null);
360: }
361:
362: public Map getWebtestProperties(final String propertyType) {
363: final String this PropType = propertyType == null ? getDefaultPropertyType()
364: : propertyType;
365:
366: if (StringUtils.isEmpty(this PropType)
367: || PROPERTY_TYPE_DYNAMIC.equals(this PropType)) {
368: return getContext().getWebtest().getDynamicProperties();
369: }
370:
371: if (this PropType.startsWith(PROPERTY_TYPE_ANT)) {
372: checkSetup();
373: return getProject().getProperties();
374: }
375: throw new StepExecutionException("Unknown propertyType: "
376: + this PropType, this );
377: }
378:
379: /**
380: * This creates a bitwise copy of the receiver. Since we do not reference
381: * any complex objects as attributes, the default implementation of
382: * object will do.
383: * The mere relay to the super implementation is left in the code as a
384: * reminder that this needs to be updated as soon as complex objects
385: * are aggregated right here or in a subclass.
386: */
387: public Object clone() throws CloneNotSupportedException {
388: return super .clone();
389: }
390:
391: protected static IStringVerifier getVerifier(final boolean useRegex) {
392: return useRegex ? RegExStringVerifier.INSTANCE
393: : EqualsStringVerfier.INSTANCE;
394: }
395:
396: /**
397: * Called if {@link #doExecute()} throws an exception
398: * @param t the thrown exception
399: */
400: protected void handleException(final Throwable t) {
401: LOG.debug("Handling exception " + t.getClass().getName() + ": "
402: + t.getMessage(), t);
403: StepUtil.handleException(t);
404: }
405:
406: /**
407: * Throw an exception if the condition holds.
408: *
409: * @param condition If true, throws the exception.
410: * @param message The error message.
411: */
412: protected void paramCheck(final boolean condition,
413: final String message) {
414: if (condition) {
415: throw new StepExecutionException(message, this );
416: }
417: }
418:
419: protected void nullParamCheck(final Object param,
420: final String paramName) {
421: paramCheck(param == null, "Required parameter \"" + paramName
422: + "\" not set!");
423: }
424:
425: protected void emptyParamCheck(final String param,
426: final String paramName) {
427: paramCheck(StringUtils.isEmpty(param), "Required parameter \""
428: + paramName + "\" not set or set to empty string!");
429: }
430:
431: /**
432: * Checks that the parameter's value is non negative
433: *
434: * @param paramName the name of the parameter
435: * @param value the parameter value
436: * @throws StepExecutionException if the value is negative
437: */
438: protected void positiveOrZeroParamCheck(final int value,
439: final String paramName) {
440: if (value < 0) {
441: throw new StepExecutionException(paramName
442: + " parameter with value '" + value
443: + "' must not be negative", this );
444: }
445: }
446:
447: protected void integerParamCheck(final String param,
448: final String paramName, final boolean nonNegative) {
449: try {
450: final int value = Integer.parseInt(param);
451: if (nonNegative && value < 0) {
452: throw new StepExecutionException(paramName
453: + " parameter with value '" + param
454: + "' must not be negative", this );
455: }
456: } catch (NumberFormatException e) {
457: throw new StepExecutionException("Can't parse " + paramName
458: + " parameter with value '" + param
459: + "' as an integer.", this );
460: }
461: }
462:
463: protected void optionalIntegerParamCheck(final String param,
464: final String paramName, final boolean nonNegative) {
465: if (!StringUtils.isEmpty(param)) {
466: integerParamCheck(param, paramName, nonNegative);
467: }
468: }
469:
470: protected void nullResponseCheck() {
471: paramCheck(getContext() == null
472: || getContext().getCurrentResponse() == null,
473: "No current response available! Is previous invoke missing?");
474: }
475:
476: /**
477: * Gets a snapshot of the values.
478: * As the value of the attributes can change over time,
479: * it is not possible to fill and cache the Map.
480: * Either fill the Map everytime, or skip the fields and use only the Map.
481: * <p>This method returns all the parameters that were discovered at build time and stored in the <em>.attributes</em> resource.
482: * Overwrite this method if your step doesn't have a <em>.attributes</em> resource
483: *
484: * @return A Map of (attribute name, attribute value) for this step.
485: */
486: public Map getParameterDictionary() {
487: final Map parameterDictionary = new TreeMap(); // to ensure order and make report comparison easier
488:
489: addComputedParameters(parameterDictionary);
490: addInternalParameters(parameterDictionary);
491:
492: return parameterDictionary;
493: }
494:
495: /**
496: * Adds parameters that are not issued from the config file but computed at runtime by the step
497: *
498: * @param map the map in which the parameters should be added
499: */
500: protected void addComputedParameters(final Map map) {
501: if (this instanceof IComputeValue) {
502: final String value = ((IComputeValue) this )
503: .getComputedValue();
504: MapUtil.putIfNotNull(map, "=> value", value);
505: }
506: }
507:
508: /**
509: * TODO: would be cleaner to notify the result listener and to give him this information
510: *
511: * @return the "results" parameter of the step
512: */
513: protected Map getComputedParameters() {
514: final Map map = new HashMap();
515: addComputedParameters(map);
516: return map;
517: }
518:
519: private void addInternalParameters(final Map map) {
520: // add internal parameters
521: MapUtil.putIfNotNull(map, "taskName", getTaskName());
522: }
523: }
|