package org.fluentlenium.core.proxy;
import org.fluentlenium.core.domain.ElementUtils;
import org.fluentlenium.core.hook.FluentHook;
import org.fluentlenium.core.hook.HookChainBuilder;
import org.fluentlenium.core.hook.HookDefinition;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.internal.WrapsElement;
import org.openqa.selenium.support.pagefactory.ElementLocator;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Abstract proxy handler supporting lazy loading and hooks on {@link WebElement}.
*
* @param <T> type of underlying object.
*/
@SuppressWarnings("PMD.GodClass")
public abstract class AbstractLocatorHandler<T> implements InvocationHandler, LocatorHandler<T> {
private static final Method TO_STRING = getMethod(Object.class, "toString");
private static final Method EQUALS = getMethod(Object.class, "equals", Object.class);
private static final Method HASH_CODE = getMethod(Object.class, "hashCode");
private static final int MAX_RETRY = 5;
private static final int HASH_CODE_SEED = 2048;
protected HookChainBuilder hookChainBuilder;
protected List<HookDefinition<?>> hookDefinitions;
private final List<ProxyElementListener> listeners = new ArrayList<>();
protected T proxy;
protected final ElementLocator locator;
protected T result;
protected List<FluentHook> hooks;
/**
* Get declared method.
*
* @param declaringClass declaring class
* @param name method name
* @param types argument types
* @return method
*/
protected static Method getMethod(Class<?> declaringClass, String name, Class... types) {
try {
return declaringClass.getMethod(name, types);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public boolean addListener(ProxyElementListener listener) {
return listeners.add(listener);
}
@Override
public boolean removeListener(ProxyElementListener listener) {
return listeners.remove(listener);
}
/**
* Fire proxy element search event.
*/
protected void fireProxyElementSearch() {
for (ProxyElementListener listener : listeners) {
listener.proxyElementSearch(proxy, locator);
}
}
/**
* Fire proxy element found event.
*
* @param result found element
*/
protected void fireProxyElementFound(T result) {
for (ProxyElementListener listener : listeners) {
listener.proxyElementFound(proxy, locator, resultToList(result));
}
}
/**
* Convert result to a list of selenium element.
*
* @param result found result
* @return list of selenium element
*/
protected abstract List<WebElement> resultToList(T result);
/**
* Creates a new locator handler.
*
* @param locator selenium element locator
*/
public AbstractLocatorHandler(ElementLocator locator) {
this.locator = locator;
}
/**
* Set the proxy using this handler.
*
* @param proxy proxy using this handler
*/
public void setProxy(T proxy) {
this.proxy = proxy;
}
/**
* Get the actual result of the locator.
*
* @return result of the locator
*/
public abstract T getLocatorResultImpl();
/**
* Get the actual result of the locator, if result is not defined and not stale.
* <p>
* It also raise events.
*
* @return result of the locator
*/
public T getLocatorResult() {
synchronized (this) {
if (result != null && isStale()) {
result = null;
}
if (result == null) {
fireProxyElementSearch();
result = getLocatorResultImpl();
fireProxyElementFound(result);
}
return result;
}
}
/**
* Get the stale status of the element.
*
* @return true if element is stale, false otherwise
*/
protected abstract boolean isStale();
/**
* Get the underlying element.
*
* @return underlying element
*/
protected abstract WebElement getElement();
/**
* Builds a {@link NoSuchElementException} with a message matching this locator handler.
*
* @return no such element exception
*/
public NoSuchElementException noSuchElement() {
return ElementUtils.noSuchElementException(getMessageContext());
}
@Override
public void setHooks(HookChainBuilder hookChainBuilder, List<HookDefinition<?>> hookDefinitions) {
if (hookDefinitions == null || hookDefinitions.isEmpty()) {
this.hookChainBuilder = null;
this.hookDefinitions = null;
hooks = null;
} else {
this.hookChainBuilder = hookChainBuilder;
this.hookDefinitions = hookDefinitions;
hooks = hookChainBuilder.build(new Supplier<WebElement>() {
@Override
public WebElement get() {
return getElement();
}
}, new Supplier<ElementLocator>() {
@Override
public ElementLocator get() {
return locator;
}
}, new Supplier<String>() {
@Override
public String get() {
return proxy.toString();
}
}, hookDefinitions);
}
}
@Override
public ElementLocator getLocator() {
return locator;
}
@Override
public ElementLocator getHookLocator() {
if (hooks != null && !hooks.isEmpty()) {
return hooks.get(hooks.size() - 1);
}
return locator;
}
@Override
public boolean loaded() {
return result != null;
}
@Override
public boolean present() {
try {
now();
} catch (TimeoutException | NoSuchElementException | StaleElementReferenceException e) {
return false;
}
return result != null && !isStale();
}
@Override
public void reset() {
result = null;
}
@Override
public void now() {
getLocatorResult();
}
@Override
@SuppressWarnings({"PMD.StdCyclomaticComplexity", "PMD.CyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity",
"PMD.NPathComplexity"})
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (TO_STRING.equals(method)) {
return proxyToString(result == null ? null : (String) invoke(method, args));
}
if (result == null) {
if (EQUALS.equals(method)) {
LocatorHandler otherLocatorHandler = LocatorProxies.getLocatorHandler(args[0]);
if (otherLocatorHandler != null) {
if (!otherLocatorHandler.loaded() || args[0] == null) {
return equals(otherLocatorHandler);
} else {
return args[0].equals(proxy);
}
}
}
if (HASH_CODE.equals(method)) {
return HASH_CODE_SEED + locator.hashCode();
}
}
if (EQUALS.equals(method)) {
LocatorHandler otherLocatorHandler = LocatorProxies.getLocatorHandler(args[0]);
if (otherLocatorHandler != null && !otherLocatorHandler.loaded()) {
otherLocatorHandler.now();
return otherLocatorHandler.equals(this);
}
}
getLocatorResult();
return invokeWithRetry(method, args);
}
//CHECKSTYLE.OFF: IllegalThrows
private Object invokeWithRetry(Method method, Object[] args) throws Throwable {
Throwable lastThrowable = null;
for (int i = 0; i < MAX_RETRY; i++) {
try {
return invoke(method, args);
} catch (StaleElementReferenceException e) {
lastThrowable = e;
reset();
getLocatorResult(); // Reload the stale element
}
}
throw lastThrowable;
}
private Object invoke(Method method, Object[] args) throws Throwable {
Object returnValue;
try {
returnValue = method.invoke(getInvocationTarget(method), args);
} catch (InvocationTargetException e) {
// Unwrap the underlying exception
throw e.getCause();
}
return returnValue;
}
//CHECKSTYLE.ON: IllegalThrows
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
AbstractLocatorHandler<?> that = (AbstractLocatorHandler<?>) obj;
return Objects.equals(locator, that.locator);
}
@Override
public int hashCode() {
return Objects.hash(locator);
}
/**
* Get string representation of not already found element.
*
* @return string representation of not already found element
*/
protected String getLazyToString() {
return "Lazy Element";
}
/**
* Get string representation of the proxy
*
* @param elementToString string representation of the underlying element
* @return string representation of the proxy
*/
public String proxyToString(String elementToString) {
if (elementToString == null) {
elementToString = getLazyToString();
}
if (locator instanceof WrapsElement) {
return elementToString;
}
return locator + " (" + elementToString + ")";
}
@Override
public String toString() {
return proxyToString(null);
}
}