/* vim: set ts=2 et sw=2 cindent fo=qroca: */
package com.globant.google.mendoza.malbec;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.HttpWebConnection;
import com.gargoylesoftware.htmlunit.KeyValuePair;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.SubmitMethod;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequestSettings;
import com.globant.google.mendoza.malbec.schema._2.CheckoutRedirect;
import com.globant.google.mendoza.malbec.schema._2.CheckoutShoppingCart;
import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.Selenium;
import com.thoughtworks.selenium.SeleniumException;
/** The simulated buyer.
*/
public class SeleniumBuyerRobot {
/** The class logger.
*/
private static Log log = LogFactory.getLog(SeleniumBuyerRobot.class);
/** This is the timeout given by Google for merchant calculations to be
* performed. The time is given in millis.
*/
private static final long MC_TIMEOUT = 3001;
/** Time to wait for bad login errors. The time is given in millis.
*/
private static final long BAD_LOGIN_TIMEOUT = 1000;
/** The url to post the cart to. This url encodes the merchant id.
*/
private String postURL;
/** The firefox-bin path.
*/
private String firefoxBinPath;
/** The firefox path. The path given to selenium to start the browser.
* Default value is "*chrome" + firefoxBinPath.
*/
private String firefoxPath;
/**
* The cart encoder.
*/
private CartPostEncoder encoder;
/**
* The merchant key used by the encoder to sign the cart.
*/
private String merchantKey;
/**
* The base url for checkout.
*/
private String serverUrl;
/**
* The buyer url for checkout.
*/
private String buyerURL;
/**
* The hostname where selenium server is running.
*/
private String seleniumHost;
/**
* The port used to connect to selenium server.
*/
private int seleniumPort;
/**
* The selenium remote controller.
*/
private DefaultSelenium seleniumRC;
/**
* Indicates if the seleniumRC is started or not.
*/
private boolean seleniumRCStarted = false;
/** Creates a simulated buyer.
*
* @param buyerUrl the buyerUrl. Cannot be null.
*
* @param url The GBuy url, including the merchant id. Cannot be null. Used
* to post carts.
*
* @param theEncoder The encoder used to encode the cart.
*
* @param theMerchantKey The merchant key used by the encoder to sign the
* cart.
*
* @param theFirefoxBinPath The path to the firefox-bin file.
*
* @param seleniumServerHost the host where selenium server is running.
*
* @param seleniumServerPort the port used to connect to selenium server.
*/
public SeleniumBuyerRobot(final String buyerUrl, final String url,
final CartPostEncoder theEncoder, final String theMerchantKey,
final String theFirefoxBinPath, final String seleniumServerHost,
final int seleniumServerPort) {
if (log.isTraceEnabled()) {
log.trace("Entering Buyer('" + url + "')");
}
if (url == null) {
throw new IllegalArgumentException("the url cannot be null");
}
if (theEncoder == null) {
throw new IllegalArgumentException("the encoder cannot be null");
}
if (theMerchantKey == null) {
throw new IllegalArgumentException("the merchant key cannot be null");
}
if (theFirefoxBinPath == null) {
throw new IllegalArgumentException("the firefox-bin path cannot be null");
}
buyerURL = buyerUrl;
postURL = url;
encoder = theEncoder;
merchantKey = theMerchantKey;
firefoxBinPath = theFirefoxBinPath;
firefoxPath = "*chrome " + firefoxBinPath;
try {
URL checkoutUrl = new URL(url);
serverUrl = checkoutUrl.getProtocol() + "://"
+ checkoutUrl.getHost()
+ (checkoutUrl.getPort() == -1 ? "" : ":" + checkoutUrl.getPort());
seleniumHost = seleniumServerHost;
seleniumPort = seleniumServerPort;
log.debug("Using firefox. Path: " + firefoxBinPath);
log.debug("Connecting with selenium server at:" + seleniumHost + ":"
+ seleniumPort);
seleniumRC = createSeleniumRc(serverUrl);
} catch (MalformedURLException e) {
throw new RuntimeException("The url received is malformed:" + url, e);
}
log.trace("Leaving Buyer");
}
/**
* Create a new default selenium, which is the selenium rc.
* @param theServerUrl The url where to star selenium interaction.
* @return Return a brand new default selenium.
*/
private DefaultSelenium createSeleniumRc(final String theServerUrl) {
DefaultSelenium newRC = new DefaultSelenium(
seleniumHost, seleniumPort, firefoxPath, theServerUrl);
return newRC;
}
/** Places an order to gbuy, specifying the cart as an JAXB cart.
*
* This function implements the buyer's actions necessary to place an order:
*
* 1- It posts the shopping cart to the gbuy url.
*
* 2- Logs in as a buyer.
*
* 3- If the initial address is specified, it posts it to the ajax url. This
* triggers the merchant calculation callback for the predefined buyer
* address.
*
* 4- It checks that the place order button is available.
*
* 5- It posts, one by one, the additional addresses or coupons/buy
* certificates to the ajax url. This triggers the merchant calculation
* callback in the server for the additional addresses, coupons and gift
* certificates.
*
* 6- Places the order, posting the coupons and gift certificates found in
* the additional addresses.
*
*
* @param cart The checkout cart to send. It cannot be null.
*
* @param email The buyer email used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param password The passoword used to log in to the GBuy service as a
* buyer. Cannot be null.
* @return returns the order number of the new posted cart order.
*
*/
public String placeOrder(final CheckoutShoppingCart cart,
final String email, final String password) {
return placeOrder(cart, email, password, null);
}
/** Places an order to gbuy, specifying the cart as an JAXB cart.
*
* This function implements the buyer's actions necessary to place an order:
*
* 1- It posts the shopping cart to the gbuy url.
*
* 2- Logs in as a buyer.
*
* 3- If the initial address is specified, it posts it to the ajax url. This
* triggers the merchant calculation callback for the predefined buyer
* address.
*
* 4- It checks that the place order button is available.
*
* 5- It posts, one by one, the additional addresses or coupons/buy
* certificates to the ajax url. This triggers the merchant calculation
* callback in the server for the additional addresses, coupons and gift
* certificates.
*
* 6- Places the order, posting the coupons and gift certificates found in
* the additional addresses.
*
*
* @param cart The checkout cart to send. It cannot be null.
*
* @param email The buyer email used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param password The passoword used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param buyerInformation A list of either shipping addresses, coupons or
* gift certificates.
*
* @return returns the order number of the new posted cart order.
*/
public String placeOrder(final CheckoutShoppingCart cart,
final String email, final String password,
final List<BuyerInformation> buyerInformation) {
return placeOrder(cart, email, password, buyerInformation, null);
}
/** Places an order to gbuy, specifying the cart as an JAXB cart.
*
* This function implements the buyer's actions necessary to place an order:
*
* 1- It posts the shopping cart to the gbuy url.
*
* 2- Logs in as a buyer.
*
* 3- If the initial address is specified, it posts it to the ajax url. This
* triggers the merchant calculation callback for the predefined buyer
* address.
*
* 4- It checks that the place order button is available.
*
* 5- It posts, one by one, the additional addresses or coupons/buy
* certificates to the ajax url. This triggers the merchant calculation
* callback in the server for the additional addresses, coupons and gift
* certificates.
*
* 6- Places the order, posting the coupons and gift certificates found in
* the additional addresses.
*
*
* @param cart The checkout cart to send. It cannot be null.
*
* @param email The buyer email used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param password The passoword used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param buyerInformation A list of either shipping addresses, coupons or
* gift certificates.
*
* @param ccOptions the list of carrier calculated shipping options to verify.
* @return returns the order number of the new posted cart order.
*/
public String placeOrder(final CheckoutShoppingCart cart,
final String email, final String password,
final List<BuyerInformation> buyerInformation,
final List<String> ccOptions) {
log.trace("Entering placeOrder");
if (cart == null) {
throw new IllegalArgumentException("the cart cannot be null");
}
if (email == null) {
throw new IllegalArgumentException("the buyer's email cannot be null");
}
if (password == null) {
throw new IllegalArgumentException("buyer's password cannot be null");
}
String returnString = postCart(email, password, buyerInformation,
encoder.generateParameters(cart, merchantKey),
encoder.getBodyForCart(cart), ccOptions);
log.trace("Leaving placeOrder");
return returnString;
}
/** Places an order to gbuy, specifying the cart as an xml string.
*
* This function implements the buyer's actions necessary to place an order:
*
* 1- It posts the shopping cart to the gbuy url.
*
* 2- Logs in as a buyer.
*
* 3- If the initial address is specified, it posts it to the ajax url. This
* triggers the merchant calculation callback for the predefined buyer
* address.
*
* 4- It checks that the place order button is available.
*
* 5- It posts, one by one, the additional addresses or coupons/buy
* certificates to the ajax url. This triggers the merchant calculation
* callback in the server for the additional addresses, coupons and gift
* certificates.
*
* 6- Places the order, posting the coupons and gift certificates found in
* the additional addresses.
*
* @param cart The checkout cart to send. It cannot be null.
*
* @param email The buyer email used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param password The passoword used to log in to the GBuy service as a
* buyer. Cannot be null.
* @return returns the order number of the new posted cart order.
*/
public String placeOrder(final String cart, final String email, final String
password) {
log.trace("Entering placeOrder");
String returnString = placeOrder(cart, email, password, null);
log.trace("Leaving placeOrder");
return returnString;
}
/** Places an order to gbuy, specifying the cart as an xml string.
*
* This function implements the buyer's actions necessary to place an order:
*
* 1- It posts the shopping cart to the gbuy url.
*
* 2- Logs in as a buyer.
*
* 3- If the initial address is specified, it posts it to the ajax url. This
* triggers the merchant calculation callback for the predefined buyer
* address.
*
* 4- It checks that the place order button is available.
*
* 5- It posts, one by one, the additional addresses or coupons/buy
* certificates to the ajax url. This triggers the merchant calculation
* callback in the server for the additional addresses, coupons and gift
* certificates.
*
* 6- Places the order, posting the coupons and gift certificates found in
* the additional addresses.
*
* @param cart The checkout cart to send. It cannot be null.
*
* @param email The buyer email used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param password The passoword used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param buyerInformation A list of either shipping addresses, coupons or
* gift certificates.
* @return returns the order number of the new posted cart order.
*/
public String placeOrder(final String cart, final String email, final String
password, final List<BuyerInformation> buyerInformation) {
return placeOrder(cart, email, password, buyerInformation, null);
}
/** Places an order to gbuy, specifying the cart as an xml string.
*
* This function implements the buyer's actions necessary to place an order:
*
* 1- It posts the shopping cart to the gbuy url.
*
* 2- Logs in as a buyer.
*
* 3- If the initial address is specified, it posts it to the ajax url. This
* triggers the merchant calculation callback for the predefined buyer
* address.
*
* 4- It checks that the place order button is available.
*
* 5- It posts, one by one, the additional addresses or coupons/buy
* certificates to the ajax url. This triggers the merchant calculation
* callback in the server for the additional addresses, coupons and gift
* certificates.
*
* 6- Places the order, posting the coupons and gift certificates found in
* the additional addresses.
*
* @param cart The checkout cart to send. It cannot be null.
*
* @param email The buyer email used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param password The passoword used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param buyerInformation A list of either shipping addresses, coupons or
* gift certificates.
*
* @param ccOptions the Carrier Calculated shipping options to verify.
* @return returns the order number of the new posted cart order.
*/
public String placeOrder(final String cart, final String email, final String
password, final List<BuyerInformation> buyerInformation,
final List<String> ccOptions) {
log.trace("Entering placeOrder");
if (cart == null) {
throw new IllegalArgumentException("the cart cannot be null");
}
List<KeyValuePair> parameters =
encoder.generateParameters(cart, merchantKey);
String body = encoder.getBodyForCart(cart);
String returnString = postCart(email, password, buyerInformation, parameters,
body, ccOptions);
log.trace("Leaving placeOrder");
return returnString;
}
/** Places an order to gbuy.
*
* @param email The buyer email used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param password The passoword used to log in to the GBuy service as a
* buyer. Cannot be null.
*
* @param buyerInformation A list of either shipping addresses, coupons or
* gift certificates.
*
* @param parameters The parameters that encode the content of the cart.
*
* @param body the body to be sent in the request.
*
* @param ccOptions The Carrier Calculated Shipping Options to verify.
* @return returns the order number of the new posted cart order.
*/
@SuppressWarnings("unchecked")
private String postCart(final String email, final String password,
final List <BuyerInformation> buyerInformation,
final List <KeyValuePair> parameters, final String body,
final List<String> ccOptions) {
try {
String path = post(parameters, body);
startSeleniumOnPage(serverUrl + path);
Thread.sleep(10000);
loginAfterPlacingOrder(email, password);
enterBuyerInformation(buyerInformation);
// Check if the Carrier Calculated Shipping Options are shown.
if (ccOptions != null && !ccOptions.isEmpty()) {
seleniumRC.waitForCondition(
"selenium.browserbot.getCurrentWindow()."
+ "document.getElementById('bottomBuyButton') != null", "30000");
seleniumRC.waitForCondition(
"selenium.browserbot.getCurrentWindow()."
+ "document.getElementById('bottomBuyButton')."
+ "disabled == false", "30000");
// First make sure that the select drop-down for the methods is present.
SeleniumUtil.waitForElementPresent(seleniumRC,
"name=shippingMethodsViewName", (int) MC_TIMEOUT);
// Then check if all elements are shown
checkCarrierCalculatedOptions(seleniumRC, ccOptions);
}
return placeOrder();
//TODO disable the comment when done.
// seleniumRC.waitForCondition(
// "selenium.browserbot.getCurrentWindow()." +
// "document.getElementById('userLinks') != null", "3000");
// seleniumRC.click("link=Sign out");
// seleniumRC.close();
// /* This code was added to handle the captcha issue on Google's internal
// * servers.
// */
// log.debug("Comparing page title: (" + orderPage.getTitleText().trim()
// + ") with (Google Accounts)");
// if (orderPage.getTitleText().trim().equals("Google Accounts")) {
// pass = (HtmlPasswordInput) form
// .getInputByName("Passwd");
// pass.setValueAttribute(password);
//
// HtmlTextInput logincaptcha =
// (HtmlTextInput) form.getInputByName("logincaptcha");
// //TODO GET FROM ARGUMENTS.
// logincaptcha.setValueAttribute("mackerel");
//
// orderPage = (HtmlPage) form.submit();
// }
} catch (FailingHttpStatusCodeException e) {
log.error("Error posting cart: " + e.getMessage());
log.debug("Error content: " + e.getResponse().getContentAsString());
throw new MalbecHttpErrorCodeException("Unable to place the order",
e.getStatusCode(), e.getResponse().getContentAsString(), e);
} catch (I18NDoesNotShipToAddressException e) {
log.debug("Found I18N exception: " + e.getMessage());
throw e;
} catch (MalbecCarrierCalculatedShippingException e) {
log.debug("Found Carrier Calculated Shipping exception: "
+ e.getMessage());
throw e;
} catch (Exception e) {
log.error("Error posting cart: " + e.getMessage(), e);
throw new RuntimeException(e);
} finally {
exit();
}
}
/**
* Starts the SeleniumRC.
*/
private void startSeleniumRC() {
log.debug("Starting Selenium RC...");
seleniumRC.start();
seleniumRCStarted = true;
log.debug("Selenium RC started");
}
/**
* Stops the SeleniumRC.
*/
public void exit() {
if (seleniumRCStarted) {
log.debug("Stopping Selenium RC...");
if (seleniumRCStarted) {
seleniumRC.stop();
seleniumRCStarted = false;
log.debug("Selenium RC stopped");
} else {
log.debug("Selenium RC was already stopped");
}
}
}
/**
* Post the cart and open the buy page with selenium.
* @param cart the Checkout Shopping cart to post. cannot be null.
*/
public void postCart(final CheckoutShoppingCart cart) {
try {
String path = post(encoder.generateParameters(cart, merchantKey),
encoder.getBodyForCart(cart));
startSeleniumOnPage(serverUrl + path);
} catch (IOException e) {
throw new RuntimeException("Could not post the cart to: " + serverUrl);
}
}
/**
* Posts a shopping cart and open the buy page with selenium.
* @param cart the shopping cart to post as a string. Cannot be null or empty.
*/
public void postCart(final String cart) {
List<KeyValuePair> parameters = encoder.generateParameters(cart,
merchantKey);
try {
String path = post(parameters, encoder.getBodyForCart(cart));
startSeleniumOnPage(serverUrl + path);
} catch (IOException e) {
throw new RuntimeException("Could not post the cart to: " + serverUrl);
}
}
/**
* Logs the user in after the order has been placed. Selenium must be started
* and the cart posted for this method to be called.
* @param email the email of the buyer to login.
* @param password the password of the buyer to login.
*/
public void loginAfterPlacingOrder(
final String email, final String password) {
if (!seleniumRCStarted) {
throw new RuntimeException("You must post the cart before logging in");
}
try {
// Find the login frame
seleniumRC.selectFrame("login");
seleniumRC.type("Email", email);
seleniumRC.type("Passwd", password);
seleniumRC.click("null");
seleniumRC.selectWindow("null");
seleniumRC.waitForPageToLoad("30000");
} catch (SeleniumException e) {
log.error("The user cannot be logged in.", e);
exit();
throw new RuntimeException("The user cannot be logged in: "
+ e.getMessage(), e);
}
boolean badEmail =
SeleniumUtil.waitForElementPresent(seleniumRC,
"//*[@id='errormsg_0_Email']", BAD_LOGIN_TIMEOUT);
boolean badLogin =
SeleniumUtil.waitForElementPresent(seleniumRC,
"//*[@id='errormsg_0_Passwd']", BAD_LOGIN_TIMEOUT);
if (badEmail || badLogin) {
exit();
throw new RuntimeException(
"Invalid login. Username and password do not match.");
}
//this forces to wait till the page is fully loaded.
SeleniumUtil.waitForElementPresent(seleniumRC, "//*[@id='addressView']",
30000);
if (SeleniumUtil.waitForTextPresent(seleniumRC,
"does not ship to this address", 5000)) {
log.trace("Found that the Merchant does not ship to the buyer's "
+ "address. Throwing an exception.");
throw new I18NDoesNotShipToAddressException("Merchant does not ship to"
+ " the buyer's address.");
}
}
/**
* Logs the user in into the user account front end, to see the purchase
* history. Selenium must be started for this method to be called.
* @param email the email of the buyer to login.
* @param password the password of the buyer to login.
*/
public void login(final String email, final String password) {
log.debug("Entering login");
if (!seleniumRCStarted) {
log.warn("Selenium must be started to log in and is down. Starting...");
startSeleniumRC();
}
try {
seleniumRC.open(buyerURL);
// Find the login frame
seleniumRC.type("Email", email);
seleniumRC.type("Passwd", password);
seleniumRC.click("null");
seleniumRC.selectWindow("null");
seleniumRC.waitForPageToLoad("30000");
boolean badEmail =
SeleniumUtil.waitForElementPresent(seleniumRC,
"//*[@id='errormsg_0_Email']", BAD_LOGIN_TIMEOUT);
boolean badLogin =
SeleniumUtil.waitForElementPresent(seleniumRC,
"//*[@id='errormsg_0_Passwd']", BAD_LOGIN_TIMEOUT);
if (badEmail || badLogin) {
exit();
throw new RuntimeException(
"Invalid login. Username and password do not match.");
}
SeleniumUtil.waitForElementPresent(
seleniumRC, "link=glob:*continue to use the service*", 3000);
if (seleniumRC.getTitle().equals("Google Accounts")) {
seleniumRC.click("link=glob:*continue to use the service*");
seleniumRC.waitForPageToLoad("30000");
}
log.debug("User is logged in now");
log.debug("Leaving login");
} catch (SeleniumException e) {
log.error("Error while accessing the user account: " + e.getMessage(), e);
exit();
throw new RuntimeException("Error while accessing the user account: "
+ e.getMessage(), e);
}
}
/**
* Gets the source code for the email message received for the new order.
* @param email The email used to log in.
* @param password the password used to log in.
* @param orderNumber the order number for the expected email message.
* @return the html source code for the email message.
*/
public String getBuyerMessageMail(final String email, final String password,
final String orderNumber) {
DefaultSelenium gmailSeleniumRC = null;
try {
String gmailUrl = "http://gmail.google.com";
gmailSeleniumRC = createSeleniumRc(gmailUrl);
gmailSeleniumRC.start();
gmailSeleniumRC.open(gmailUrl);
gmailSeleniumRC.waitForPageToLoad("30000");
//login
gmailSeleniumRC.type("Email", email);
gmailSeleniumRC.type("Passwd", password);
gmailSeleniumRC.click("name=signIn");
gmailSeleniumRC.waitForPageToLoad("30000");
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
log.warn("Interrupted sleep", e);
}
gmailSeleniumRC.selectFrame("main");
gmailSeleniumRC.selectFrame("v1");
gmailSeleniumRC.waitForCondition(
"selenium.browserbot.getCurrentWindow()"
+ ".document.getElementsByName('q')[0].value='" + orderNumber + "'",
"1000");
gmailSeleniumRC.submit("id=s");
//open result if any
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
log.warn("Interrupted sleep", e);
}
gmailSeleniumRC.keyPress("xpath=//body", "o");
String result = gmailSeleniumRC.getHtmlSource();
return result;
} finally {
if (gmailSeleniumRC != null) {
gmailSeleniumRC.stop();
}
}
}
/**
* Gets the order page for the given order number. You must call login first.
* @param orderNumber the order number.
* @return the order page.
*/
public String getBuyerOrderInformation(final String orderNumber) {
log.debug("Entering getBuyerOrderInformation");
if (!seleniumRCStarted) {
throw new RuntimeException("The selenium must be started to get the order"
+ " information");
}
try {
String orderPage = buyerURL.endsWith("/") ? buyerURL : buyerURL + "/";
orderPage += "view/receipt";
Map<String, String> parameters = new HashMap<String, String>();
parameters.put("t", orderNumber);
log.debug("Leaving getBuyerOrderInformation");
return getPage(orderPage, parameters);
} catch (SeleniumException e) {
log.error("Found an error while getting the order information from the"
+ " buyer page: " + e.getMessage(), e);
exit();
throw new RuntimeException("Could not get the order information: "
+ e.getMessage(), e);
}
}
/**
* Starts selenium on the buy page using the given path.
* @param path the path to access the buy page.
*/
private void startSeleniumOnPage(final String path) {
startSeleniumRC();
try {
seleniumRC.setTimeout("5000");
seleniumRC.open(path);
} catch (Exception e) {
//HACK
}
seleniumRC.setTimeout("30000");
}
/**
* Gets the html source obtained when requesting an url with a set of
* parameters.
* @param url the url to request.
* @param parameters the parameters to be posted.
* @return the html source of the obtained page.
*/
private String getPage(
final String url, final Map<String, String> parameters) {
String result = url;
if (parameters.keySet().isEmpty()) {
seleniumRC.open(result);
return seleniumRC.getHtmlSource();
}
result += "?";
for (Iterator<String> it = parameters.keySet().iterator(); it.hasNext();) {
String key = it.next();
result += key + "=" + parameters.get(key);
result += "&";
}
result = result.substring(0, result.length() - 1);
seleniumRC.open(result);
return seleniumRC.getHtmlSource();
}
/**
* Enters the list of buyerInformation objects given, such as coupons or
* address changes, for example.
* @param buyerInformation the list of BuyerInformation objects.
*/
public void enterBuyerInformation(
final List<BuyerInformation> buyerInformation) {
try {
// wait for the initial mc if there is any.
Thread.sleep(MC_TIMEOUT);
// Enter addresses, coupons and gift certificates.
if (buyerInformation != null) {
BuyerVisitor buyerVisitor = new SeleniumBuyerVisitor(seleniumRC);
for (BuyerInformation information : buyerInformation) {
information.execute(buyerVisitor);
Thread.sleep(MC_TIMEOUT);
}
}
} catch (InterruptedException e) {
throw new RuntimeException("Error entering buyer information", e);
}
}
/**
* Place an order. Click on the buy button. Should be called once the selenium
* was started and the user has logged in.
* @return the order number of the placed order.
*/
public String placeOrder() {
try {
if (!seleniumRCStarted) {
throw new RuntimeException("Selenium must be started before placing"
+ " the order");
}
// Check that the button exists on the page.
try {
seleniumRC.waitForCondition(
"selenium.browserbot.getCurrentWindow()."
+ "document.getElementById('bottomBuyButton') != null", "30000");
} catch (SeleniumException e) {
log.error("The button is not visible on the current page. "
+ "Check that the user is logged in before placing the order", e);
exit();
throw new RuntimeException("Must be logged in to place an order", e);
}
// Check that the button is available to be clicked.
try {
seleniumRC.waitForCondition(
"selenium.browserbot.getCurrentWindow()."
+ "document.getElementById('bottomBuyButton')."
+ "disabled == false", "30000");
} catch (SeleniumException e) {
log.error("The button to place order is disabled.", e);
exit();
throw new RuntimeException("Could not place the order: "
+ "button is disabled", e);
}
// Even if the bottomBuyButton is enabled just wait 2 seconds (looks like
// a messy hack to emulate the human interaction with the UI) this is
// done because when entering coupons the bottomBuyButton is enabled
// before the coupon timeout expired.
Thread.sleep(2000);
// Place the order
log.debug("Placing order");
seleniumRC.click("bottomBuyButton");
seleniumRC.waitForCondition(
"selenium.getTitle() == 'Purchase Complete!'", "45000");
return getOrderNumber(seleniumRC.getLocation());
} catch (InterruptedException e) {
exit();
throw new RuntimeException("Error placing the order", e);
}
}
/**
* Get the current page html.
* @return the current page html.
*/
public String getPage() {
if (!seleniumRCStarted) {
throw new RuntimeException(
"Cannot obtain a page when selenium is stopped");
}
try {
return seleniumRC.getHtmlSource();
} catch (SeleniumException e) {
log.error("The current page could not be obtained: " + e.getMessage(),
e);
exit();
throw new RuntimeException("Could not obtain page: " + e.getMessage(),
e);
}
}
/**
* Get the buyer receipt as html. Should be called once the order was placed.
* @return the buyer receipt as html.
*/
public String getBuyerReceipt() {
try {
Thread.sleep(3000);
SeleniumUtil.waitForTextPresent(
seleniumRC, "Get up-to-date order progress", 3000);
seleniumRC.click("link=Get up-to-date order progress");
return seleniumRC.getHtmlSource();
} catch (InterruptedException e) {
throw new RuntimeException("Error trying to get the buyer receipt", e);
}
}
/**
* Gets the order number based on a url, the order number comes as a value of
* the t get parameter.
* @param url The url that contains the order number.
* @return The order number.
*/
private String getOrderNumber(final String url) {
log.trace("getting ordernumber from " + url);
int position = url.indexOf("t=");
if (position != -1) {
String orderNumber = url.substring(position + 2);
int ampIndex = url.indexOf("&");
boolean moreParameters = (ampIndex != -1);
if (moreParameters) {
orderNumber = url.substring(position + 2, ampIndex);
}
return orderNumber;
} else {
throw new RuntimeException("The order number couldnt be found in the url"
+ " after placing the order");
}
}
/**
* Checks if all the given carrier calculated shipping options are shown in
* the place order page.
* @param selenium the {@link Selenium} class to use.
* @param options the shipping option names to check.
*/
private void checkCarrierCalculatedOptions(final Selenium selenium,
final List<String> options) {
List<String> foundOptions = Arrays
.asList(selenium.getSelectOptions("name=shippingMethodsViewName"));
for (String current : options) {
if (!anyStartWith(foundOptions, current + " (")) {
throw new MalbecCarrierCalculatedShippingException("Carrier calculated"
+ "shipping option not found: " + current);
}
}
}
/**
* Checks if any of the strings in the container starts with the given prefix.
* @param container The container to iterate.
* @param prefix the prefix to be found.
* @return true if at least one element is found.
*/
private boolean anyStartWith(final List<String> container,
final String prefix) {
for (String current : container) {
if (current.startsWith(prefix)) {
return true;
}
}
return false;
}
/**
* Posts the shopping cart and get the response.
* @param parameters The parameters that encode the content of the cart
* @param body the Request body to be sent.
* @return Returns the path to the place order page.
* @throws IOException when the URL is invalid, or any IO error occurs.
*/
private String post(final List<KeyValuePair> parameters, final String body)
throws IOException {
if (body != null) {
return postUnsigned(parameters, body);
} else {
return postSigned(parameters);
}
}
/**
* Posts the signed shopping cart and get the response.
* @param parameters The parameters that encode the content of the cart
* @return Returns the path to the place order page.
* @throws IOException when the URL is invalid, or any IO error occurs.
*/
private String postSigned(final List<KeyValuePair> parameters)
throws IOException {
WebClient client = createWebClient();
log.debug("Posting shopping cart to " + postURL);
WebRequestSettings request = new WebRequestSettings(new URL(postURL),
SubmitMethod.POST);
request.setRequestParameters(parameters);
log.debug(parameters.toString());
try {
Page page = client.getPage(request);
String path = page.getWebResponse().getUrl().getFile();
return path;
} catch (Exception e) {
//throw new RuntimeException("Could not connect to " + postURL);
throw new RuntimeException(e);
}
}
/**
* Posts the shopping cart and get the response.
* @param parameters The parameters that encode the content of the cart
* @param body the Request body to be sent.
* @return Returns the path to the place order page.
* @throws IOException when the URL is invalid, or an IO error occurs.
*/
@SuppressWarnings("unchecked")
private String postUnsigned(final List<KeyValuePair> parameters,
final String body)
throws IOException {
WebClient client = createWebClient();
log.debug("Posting shopping cart to " + postURL);
WebRequestSettings request = new WebRequestSettings(new URL(postURL),
SubmitMethod.POST);
request.setRequestBody(body);
Map headers = new HashMap<String, String>();
for (KeyValuePair current : parameters) {
headers.put(current.getKey(), current.getValue());
}
request.setAdditionalHeaders(headers);
try {
Page page = client.getPage(request);
CheckoutRedirect response = (CheckoutRedirect) CartUtils.unmarshal(
page.getWebResponse().getContentAsString()).getValue();
URL url = new URL(URLDecoder.decode(response.getRedirectUrl(), "UTF-8"));
String path = url.getFile();
return path;
} catch (Exception e) {
throw new RuntimeException("Could not connect to " + postURL);
}
}
/** Creates the WebClient that will be used by this BuyerRobot.
*
* @return The web client.
*/
private WebClient createWebClient() {
WebClient client = new WebClient();
CookieWebConnection cookieWebConnection = new CookieWebConnection(client);
client.setWebConnection(cookieWebConnection);
client.setJavaScriptEnabled(false);
return client;
}
/** An object that handles the actual communication portion of page
* retrieval/submission.
*
* This connection sets the cookie policy as BROWSER_COMPATIBILITY.
*
* BROWSER_COMPATIBILITY: compatible with the common cookie management
* practices (even if they are not 100% standards compliant)
*/
private static class CookieWebConnection extends HttpWebConnection {
/** Creates a CookieWebConnection.
*
* @param webClient The web client.
*/
public CookieWebConnection(final WebClient webClient) {
super(webClient);
}
/** Creates the httpClient that will be used by this CookieWebConnection.
*
* @return the client
*/
protected HttpClient createHttpClient() {
HttpClientParams httpClientParams = new HttpClientParams();
httpClientParams.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
HttpClient httpClient = new HttpClient(httpClientParams);
return httpClient;
}
}
/** Creates a sample html page suitable to show in a browser and manually
* post the shopping cart.
*
* @param cart The shopping cart to be posted.
*
* @return Returns a string with an html page that can be used to manually
* post the order.
*/
public String createHtmlPageForOrder(final CheckoutShoppingCart cart) {
StringBuffer page = new StringBuffer();
page.append("<html>\n");
page.append("<body>\n");
page.append(" <form method='post' action='").append(postURL).append("'>\n");
List<KeyValuePair> parameters =
encoder.generateParameters(cart, merchantKey);
for (Iterator it = parameters.iterator(); it.hasNext();) {
KeyValuePair pair = (KeyValuePair) it.next();
page.append(" <input type='hidden' name='").append(pair.getKey()).
append("' value='").append(pair.getValue()).append("'>\n");
}
page.append(" Post the cart:\n");
page.append(" <input type='submit' name='Post'>\n");
page.append(" </form>\n");
page.append("</body>\n");
page.append("</html>\n");
return page.toString();
}
/**
* Click the anchor that has the text received as parameter.
* @param linkText The anchor text.
* @param timeout the timeout to wait once link was clicked.
*/
public void linkText(final String linkText, final long timeout) {
SeleniumUtil.waitForElementPresent(seleniumRC, "link=" + linkText, 3000);
seleniumRC.click("link=" + linkText);
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* Click the anchor that has the id attribute received as parameter.
* @param linkId The anchor id attribute.
* @param timeout the timeout to wait once link was clicked.
*/
public void linkId(final String linkId, final long timeout) {
SeleniumUtil.waitForElementPresent(seleniumRC, "id=" + linkId, 3000);
seleniumRC.click("id=" + linkId);
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* Gets the text of an element. This works for any element that contains text.
* @param elementId The element id attribute.
* @param timeoutMillis The timeout in milliseconds to wait to get the element
* text.
* @return The text of the element.
*/
public String getElementContent(final String elementId,
final int timeoutMillis) {
SeleniumUtil.waitForElementPresent(
seleniumRC, "id=" + elementId, timeoutMillis);
return seleniumRC.getText("id=" + elementId);
}
}