RPA
Advantages of WorkFusion Bot Config Bundle approach
With a BCB project used as the ODF base, you take all of the advantages that Java can give in terms of RPA.
Some major advantages are:
- WorkFusion extended driver allowing to automate various Windows desktop applications, SAP applications, perform surface RPA
- Ability to easily use Page Object pattern
- Easy source code availability
- Reliable IDE autocompletion
WorkFusion has the com.workfusion.rpa.helpers.RPA class with plenty of static methods that are imported by default in scripts of XML bot
configs:
RPA#driverto get current WebDriverRPA#$to find single UiElementRPA#$$to find multiple UiElements and much more
tip
Make sure you learn WorkFusion-provided com.workfusion.rpa.helpers.RPA:
How to start with BCB
To start working with RPA, first of all you have to start the RPA Bot. Currently, you cannot do it from Java, you should do it as you using robotics plugins.
When you are inside of the <robot> block, WebDriver is available for your manipulations. You can pass it as a Java constructor parameter, but a better way is to use the static RPA#driver method. That way, you can get WebDriver anywhere in your code.
After that, you can start your Selenium WebDriver routine.
Transaction Supplier in Bot Task
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://web-harvest.sourceforge.net/schema/1.0/config" scriptlang="groovy">
<robotics-flow>
<robot driver="chrome" close-on-completion="true" >
<script></script>
</robot>
</robotics-flow>
<export include-original-data="false">
<multi-column list="${result}" split-results="true">
<put-to-column-getter name="_sys_transaction_id" property="transaction_id"/>
</multi-column>
</export>
</config>
TransactionSupplier
package com.workfusion.intakequickstart.supplier;
import com.workfusion.intake.api.connector.TransactionSupplier;
import com.workfusion.intake.api.domain.Document;
import com.workfusion.intake.api.domain.Transaction;
import com.workfusion.intakequickstart.supplier.rpa.ProductsParser;
import com.workfusion.rpa.core.security.SecurityUtils;
import org.slf4j.Logger;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
public class TransactionSupplierRPAExample implements TransactionSupplier {
private final SecurityUtils securityUtils;
private final Logger logger ;
/**
* To enable dependency injection, you need to apply @Inject annotation to your constructor.
* @param logger will be injected without any configuration,
* @param securityUtils injection was configured in com.workfusion.intakequickstart.core.SecurityModule
* */
@Inject
public TransactionSupplierRPAExample(final SecurityUtils securityUtils, final Logger logger) {
this.logger=logger;
this.securityUtils=securityUtils;
}
@Override
public Collection<Transaction> get() {
Collection<Transaction> transactions = new ArrayList<>();
Transaction transaction = new Transaction();
transaction.setId(uuid());
transaction.setDocs(createDocuments());
transactions.add(transaction);
return transactions;
}
private List<Document> createDocuments() {
final List<Document> collect = new ProductsParser(securityUtils, logger).parseProductsToDocuments();
return collect;
}
private String uuid() {
return UUID.randomUUID().toString();
}
}
ProductsParser
package com.workfusion.intakequickstart.supplier.rpa;
import com.workfusion.bot.service.SecureEntryDTO;
import com.workfusion.intake.api.domain.Document;
import com.workfusion.intake.api.domain.Field;
import com.workfusion.rpa.core.security.SecurityUtils;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class ProductsParser {
public static final String EXAMPL_ALIAS = "exampl_alis";
private final SecurityUtils securityUtils;
private final Logger logger ;
private static final int EXPECTED_PRODUCTS_COUNT = 20;
private MenuNavigationBar menuNavigationBar ;
public ProductsParser(final SecurityUtils securityUtils, final Logger logger) {
this.logger=logger;
this.securityUtils=securityUtils;
}
public List<Document> parseProductsToDocuments() {
initRobot();
final List<ProductTO> products = parseProducts();
logger.debug("Extracted products count: " + products.size());
finiliseRobot();
return products
.stream()
.map(this::mapProductToDocument)
.limit(EXPECTED_PRODUCTS_COUNT)
.collect(Collectors.toList());
}
private MainPage initRobot( ) {
final SecureEntryDTO loginCreds = securityUtils.getSecureEntry(EXAMPL_ALIAS);
InvoicePlaneClient client = new InvoicePlaneClient();
LoginPage loginPage = client.getLoginPage();
MainPage mainPage = loginPage.login(loginCreds);
this.menuNavigationBar = new MenuNavigationBar();
return mainPage;
}
private List<ProductTO> parseProducts() {
final ProductsPage productsPage = menuNavigationBar.openProducts();
final List<ProductTO> products = new ArrayList();
while (needMoreProductsAndHasSmthToParse( productsPage, products)) {
products.addAll(productsPage.getProducts().stream().filter(distinctByKey(p -> p.getProductName().toLowerCase())).collect(Collectors.toList()));
}
return products;
}
private String uuid() {
return UUID.randomUUID().toString();
}
private void finiliseRobot() {
if (menuNavigationBar != null) {
menuNavigationBar.logout();
menuNavigationBar=null;
}
}
private <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
Map<Object, Boolean> map = new ConcurrentHashMap<>();
return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}
private boolean needMoreProductsAndHasSmthToParse(ProductsPage productsPage, List<ProductTO> products) {
return !(products.size() > EXPECTED_PRODUCTS_COUNT || !productsPage.nextPage());
}
private Document mapProductToDocument(ProductTO productTO){
Document document = new Document();
document.setId(uuid());
document.setName(productTO.getProductName());
document.getExtractedFields().put("family", Field.of(productTO.getFamily()) );
document.getExtractedFields().put("price", Field.of(productTO.getPrice()) );
document.getExtractedFields().put("description", Field.of(productTO.getProductDescription()) );
document.getExtractedFields().put("product_name", Field.of(productTO.getProductName()) );
document.getExtractedFields().put("sku", Field.of(productTO.getSku()) );
document.getExtractedFields().put("tax_rate", Field.of(productTO.getTaxRate()) );
document.getExtractedFields().put("index", Field.of(Long.toString(productTO.getIndex())) );
return document;
}
}
RobotDriverWrapper
package com.workfusion.intakequickstart.supplier.rpa;
import com.workfusion.rpa.helpers.RPA;
import org.openqa.selenium.support.PageFactory;
import org.slf4j.Logger;
public class RobotDriverWrapper {
protected final Logger logger ;
public RobotDriverWrapper(final Logger logger ) {
PageFactory.initElements(RPA.driver(), this);
this.logger=logger;
}
}
InvoicePlaneClient
package com.workfusion.intakequickstart.supplier.rpa;
import org.slf4j.Logger;
import java.util.concurrent.TimeUnit;
import static com.workfusion.rpa.helpers.RPA.driver;
public class InvoicePlaneClient extends RobotDriverWrapper {
public static final String HTTP_INVOICEPLANE_WORKFUSION_COM = "http://invoiceplane.workfusion.com";
public InvoicePlaneClient(Logger logger) {
super(logger);
initDriver();
}
private void initDriver() {
driver().manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS).pageLoadTimeout(90, TimeUnit.SECONDS);
driver().manage().deleteAllCookies();
}
public LoginPage getLoginPage() {
driver().navigate().to(HTTP_INVOICEPLANE_WORKFUSION_COM);
return new LoginPage(logger);
}
}
LoginPage
package com.workfusion.intakequickstart.supplier.rpa;
import com.workfusion.bot.service.SecureEntryDTO;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.slf4j.Logger;
public class LoginPage extends RobotDriverWrapper {
private static final int WAIT_FIELD = 20;
@FindBy(id = "email")
private WebElement email;
@FindBy(id = "password")
private WebElement password;
@FindBy(xpath = "//input[@type='submit']")
private WebElement submit;
@FindBy(xpath = "//*[self::div[@id='main-area'] or self::div[@id='login']/div[contains(@class,'alert')]]")
private WebElement loginFailed;
@FindBy(xpath = "//div[@id='login']/div[contains(@class,'alert')]")
private WebElement loginFailedMessage;
public LoginPage(Logger logger) {
super(logger);
}
public MainPage login(SecureEntryDTO invoicePlaneCred) {
email.click();
email.clear();
email.sendKeys(invoicePlaneCred.getKey());
password.click();
password.clear();
password.sendKeys(invoicePlaneCred.getValue());
submit.click();
try {
String actualId = loginFailed.getAttribute("id");
logger.debug(actualId);
if (!"main-area".equalsIgnoreCase(actualId)) {
throw new RuntimeException(loginFailedMessage.getText() + "\n" + "User Id: " + invoicePlaneCred.getKey());
}
} catch (TimeoutException e) {
logger.debug("Unkown error during Invoice Place authorisation process.");
throw new RuntimeException();
}
return new MainPage(logger);
}
}
MainPage
package com.workfusion.intakequickstart.supplier.rpa;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.slf4j.Logger;
public class MainPage extends RobotDriverWrapper {
@FindBy(xpath = "//div[@id='panel-quick-actions']//a//span[text()='Add Client']/parent::*")
private WebElement addClientButton;
public MainPage(Logger logger) {
super(logger);
}
public void addClient() {
addClientButton.click();
}
public MenuNavigationBar getMenuNavigationBar(){
return new MenuNavigationBar(logger);
}
}
MenuNavigationBar
package com.workfusion.intakequickstart.supplier.rpa;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.slf4j.Logger;
public class MenuNavigationBar extends RobotDriverWrapper {
@FindBy(xpath = "//a[contains(@class,'logout')]")
private WebElement logoutButton;
@FindBy(xpath = "//*[@id='ip-navbar-collapse']//li/a[text()='Dashboard']")
private WebElement dashboardMenu;
@FindBy(xpath = "//*[@id='ip-navbar-collapse']//li/a//span[text()='Products']/parent::*")
private WebElement productsMenu;
@FindBy(xpath = "//*[@id='ip-navbar-collapse']//li/ul/li/a[text()='View products']")
private WebElement viewProductsMenuItem;
public MenuNavigationBar(Logger logger) {
super(logger);
}
public ProductsPage openProducts() {
productsMenu.click();
viewProductsMenuItem.click();
return new ProductsPage(logger);
}
public void openDashboard() {
dashboardMenu.click();
}
// If it is necessary to logout from Invoice Plane explicitly
public void logout() {
try {
logoutButton.click();
} catch (TimeoutException ex) {
logger.info("Timed out on waiting logout button");
}
}
}
ProductTO
package com.workfusion.intakequickstart.supplier.rpa;
import com.freedomoss.workfusion.utils.gson.GsonUtils;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
public class ProductTO {
@SerializedName("index")
@Expose
private long index;
@SerializedName("family")
@Expose
private String family;
@SerializedName("sku")
@Expose
private String sku;
@SerializedName("product_name")
@Expose
private String productName;
@SerializedName("product_description")
@Expose
private String productDescription;
@SerializedName("price")
@Expose
private String price;
@SerializedName("tax_rate")
@Expose
private String taxRate;
public String getFamily() {
return family;
}
public void setFamily(String family) {
this.family = family;
}
public String getSku() {
return sku;
}
public void setSku(String sku) {
this.sku = sku;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getProductDescription() {
return productDescription;
}
public void setProductDescription(String productDescription) {
this.productDescription = productDescription;
}
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
public String getTaxRate() {
return taxRate;
}
public void setTaxRate(String taxRate) {
this.taxRate = taxRate;
}
public long getIndex() {
return index;
}
public void setIndex(long index) {
this.index = index;
}
public String toJson() {
return GsonUtils.GSON.toJson(this);
}
}
ProductsPage
package com.workfusion.intakequickstart.supplier.rpa;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.parser.Parser;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ProductsPage extends RobotDriverWrapper {
@FindBy(xpath = "//div[@id='content']//table/tbody/tr")
private List<WebElement> products;
@FindBy(xpath = "//div[@id='headerbar']/div[@class='pull-right']//a[@title='First' and not(contains(@class,'disabled'))]")
private WebElement firstPage;
@FindBy(xpath = "//div[@id='headerbar']/div[@class='pull-right']//a[@title='Last' and not(contains(@class,'disabled'))]")
private WebElement lastPage;
@FindBy(xpath = "//div[@id='headerbar']/div[@class='pull-right']//a[@title='Next' and not(contains(@class,'disabled'))]")
private WebElement nextPage;
@FindBy(xpath = "//div[@id='headerbar']/div[@class='pull-right']//a[@title='Prev' and not(contains(@class,'disabled'))]")
private WebElement prevPage;
public ProductsPage(Logger logger) {
super(logger);
}
public List<ProductTO> getProducts() {
final List<ProductTO> result;
if (products != null) {
result= IntStream
.range(0,products.size())
.mapToObj((int index) -> mapWebElementToProduct(products.get(index), index))
.collect(Collectors.toList());
}
else {
result = new ArrayList<>();
}
return result;
}
private ProductTO mapWebElementToProduct(WebElement product, long index) {
Document doc = Jsoup.parse(product.getAttribute("outerHTML"), "", Parser.xmlParser());
ProductTO productTO = new ProductTO();
productTO.setIndex(index);
productTO.setFamily(doc.select("td:nth-child(1)").text());
productTO.setSku(doc.select("td:nth-child(2)").text());
productTO.setProductName(doc.select("td:nth-child(3)").text());
productTO.setProductDescription(doc.select("td:nth-child(4)").text());
productTO.setPrice(doc.select("td:nth-child(5)").text());
productTO.setTaxRate(doc.select("td:nth-child(6)").text());
return productTO;
}
public boolean firstPage() {
try {
firstPage.click();
return true;
} catch (WebDriverException e) {
return false;
}
}
public boolean lastPage() {
try {
lastPage.click();
return true;
} catch (WebDriverException e) {
return false;
}
}
public boolean nextPage() {
try {
nextPage.click();
return true;
} catch (WebDriverException e) {
return false;
}
}
public boolean prevPage() {
try {
prevPage.click();
return true;
} catch (WebDriverException e) {
return false;
}
}
}
Switch between applications
Another handy feature worth mentioning is org.openqa.selenium.WebDriver#switchDriver. With its help, you can switch between desktop and web drivers inside of the universal driver, without using a separate desktop and web drivers.