package org.fluentlenium.core.inject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.lang3.ArrayUtils;
import org.fluentlenium.core.FluentContainer;
import org.fluentlenium.core.FluentControl;
import org.fluentlenium.core.annotation.Page;
import org.fluentlenium.core.components.ComponentsManager;
import org.fluentlenium.core.components.LazyComponents;
import org.fluentlenium.core.components.LazyComponentsListener;
import org.fluentlenium.core.domain.ComponentList;
import org.fluentlenium.core.domain.FluentList;
import org.fluentlenium.core.domain.FluentWebElement;
import org.fluentlenium.core.events.ContainerAnnotationsEventsRegistry;
import org.fluentlenium.core.events.EventsRegistry;
import org.fluentlenium.core.hook.DefaultHookChainBuilder;
import org.fluentlenium.core.hook.FluentHook;
import org.fluentlenium.core.hook.Hook;
import org.fluentlenium.core.hook.HookControlImpl;
import org.fluentlenium.core.hook.HookDefinition;
import org.fluentlenium.core.hook.HookOptions;
import org.fluentlenium.core.hook.NoHook;
import org.fluentlenium.core.proxy.LocatorProxies;
import org.fluentlenium.utils.ReflectionUtils;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.pagefactory.ElementLocator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
/**
* Handle injection of element proxies, @Page objects and @FindBy.
*/
@SuppressWarnings("PMD.GodClass")
public class FluentInjector implements FluentInjectControl {
private final Map<Class, Object> containerInstances = new IdentityHashMap<>();
private final Map<Object, ContainerContext> containerContexts = new IdentityHashMap<>();
private final Map<Object, ContainerAnnotationsEventsRegistry> eventsContainerSupport = new IdentityHashMap<>();
private final FluentControl fluentControl;
private final ComponentsManager componentsManager;
private final ContainerInstanciator containerInstanciator;
private final DefaultHookChainBuilder hookChainBuilder;
private final EventsRegistry eventsRegistry;
/**
* Creates a new injector.
*
* @param control control interface
* @param eventsRegistry events registry
* @param componentsManager components manager
* @param instanciator container instantiator
*/
public FluentInjector(FluentControl control, EventsRegistry eventsRegistry, ComponentsManager componentsManager,
ContainerInstanciator instanciator) {
fluentControl = control;
this.eventsRegistry = eventsRegistry;
this.componentsManager = componentsManager;
containerInstanciator = instanciator;
hookChainBuilder = new DefaultHookChainBuilder(control, componentsManager.getInstantiator());
}
/**
* Release all loaded containers.
*/
public void release() {
containerInstances.clear();
for (ContainerAnnotationsEventsRegistry support : eventsContainerSupport.values()) {
support.close();
}
eventsContainerSupport.clear();
containerContexts.clear();
componentsManager.release();
}
@Override
public <T> T newInstance(Class<T> cls) {
T container = containerInstanciator.newInstance(cls, null);
inject(container);
return container;
}
@Override
public ContainerContext inject(Object container) {
inject(container, null, fluentControl.getDriver());
return containerContexts.get(container);
}
@Override
public ContainerContext injectComponent(Object componentContainer, Object parentContainer, SearchContext searchContext) {
initContainerContext(componentContainer, parentContainer, searchContext);
initParentContainer(componentContainer, parentContainer);
initFluentElements(componentContainer, searchContext);
initChildrenContainers(componentContainer, searchContext);
return containerContexts.get(componentContainer);
}
private void inject(Object container, Object parentContainer, SearchContext searchContext) {
initContainer(container, parentContainer, searchContext);
initParentContainer(container, parentContainer);
initFluentElements(container, searchContext);
initChildrenContainers(container, searchContext);
}
private void initParentContainer(Object container, Object parentContainer) {
for (Class cls = container.getClass(); isClassSupported(cls); cls = cls.getSuperclass()) {
for (Field field : cls.getDeclaredFields()) {
if (isParent(field)) {
try {
ReflectionUtils.set(field, container, parentContainer);
} catch (IllegalAccessException | IllegalArgumentException e) {
throw new FluentInjectException("Can't set field " + field + " with value " + parentContainer, e);
}
}
}
}
}
private boolean isParent(Field field) {
return field.isAnnotationPresent(Parent.class);
}
private void initContainer(Object container, Object parentContainer, SearchContext searchContext) {
initContainerContext(container, parentContainer, searchContext);
if (container instanceof FluentContainer) {
((FluentContainer) container).initFluent(new ContainerFluentControl(fluentControl, containerContexts.get(container)));
}
initEventAnnotations(container);
}
private void initContainerContext(Object container, Object parentContainer, SearchContext searchContext) {
ContainerContext parentContainerContext = parentContainer == null ? null : containerContexts.get(parentContainer);
DefaultContainerContext containerContext = new DefaultContainerContext(container, parentContainerContext, searchContext);
containerContexts.put(container, containerContext);
if (parentContainerContext != null) {
containerContext.getHookDefinitions().addAll(parentContainerContext.getHookDefinitions());
}
for (Class cls = container.getClass(); isClassSupported(cls); cls = cls.getSuperclass()) {
addHookDefinitions(cls.getDeclaredAnnotations(), containerContext.getHookDefinitions());
}
}
private void initEventAnnotations(Object container) {
if (eventsRegistry != null && !eventsContainerSupport.containsKey(container)) {
eventsContainerSupport.put(container, new ContainerAnnotationsEventsRegistry(eventsRegistry, container));
}
}
private static boolean isContainer(Field field) {
return field.isAnnotationPresent(Page.class);
}
private static boolean isClassSupported(Class<?> cls) {
return cls != Object.class && cls != null;
}
private void initChildrenContainers(Object container, SearchContext searchContext) {
for (Class cls = container.getClass(); isClassSupported(cls); cls = cls.getSuperclass()) {
for (Field field : cls.getDeclaredFields()) {
if (isContainer(field)) {
Class fieldClass = field.getType();
Object existingChildContainer = containerInstances.get(fieldClass);
if (existingChildContainer == null) {
Object childContainer = containerInstanciator.newInstance(fieldClass, containerContexts.get(container));
initContainer(childContainer, container, searchContext);
try {
ReflectionUtils.set(field, container, childContainer);
} catch (IllegalAccessException e) {
throw new FluentInjectException("Can't set field " + field + " with value " + childContainer, e);
}
containerInstances.put(fieldClass, childContainer);
inject(childContainer, container, searchContext);
} else {
try {
ReflectionUtils.set(field, container, existingChildContainer);
} catch (IllegalAccessException e) {
throw new FluentInjectException("Can't set field " + field + " with value " + existingChildContainer,
e);
}
}
}
}
}
}
private void initFluentElements(Object container, SearchContext searchContext) {
ContainerContext containerContext = containerContexts.get(container);
for (Class cls = container.getClass(); isClassSupported(cls); cls = cls.getSuperclass()) {
for (Field field : cls.getDeclaredFields()) {
if (isSupported(container, field)) {
ArrayList<HookDefinition<?>> fieldHookDefinitions = new ArrayList<>(containerContext.getHookDefinitions());
addHookDefinitions(field.getAnnotations(), fieldHookDefinitions);
InjectionElementLocatorFactory locatorFactory = new InjectionElementLocatorFactory(searchContext);
InjectionElementLocator locator = locatorFactory.createLocator(field);
if (locator != null) {
ComponentAndProxy fieldValue = initFieldElements(locator, field);
injectComponent(fieldValue, locator, container, field, fieldHookDefinitions);
}
}
}
}
}
private void injectComponent(ComponentAndProxy fieldValue, ElementLocator locator, Object container, Field field,
ArrayList<HookDefinition<?>> fieldHookDefinitions) {
if (fieldValue != null) {
LocatorProxies.setHooks(fieldValue.getProxy(), hookChainBuilder, fieldHookDefinitions);
try {
ReflectionUtils.set(field, container, fieldValue.getComponent());
} catch (IllegalAccessException e) {
throw new FluentInjectException(
"Unable to find an accessible constructor with an argument of type WebElement in " + field.getType(), e);
}
if (fieldValue.getComponent() instanceof Iterable) {
if (isLazyComponentsAndNotInitialized(fieldValue.getComponent())) {
LazyComponents lazyComponents = (LazyComponents) fieldValue.getComponent();
lazyComponents.addLazyComponentsListener(new LazyComponentsListener<Object>() {
@Override
public void lazyComponentsInitialized(Map<WebElement, Object> componentMap) {
for (Entry<WebElement, Object> componentEntry : componentMap.entrySet()) {
injectComponent(componentEntry.getValue(), container, componentEntry.getKey());
}
}
});
}
} else {
ElementLocatorSearchContext componentSearchContext = new ElementLocatorSearchContext(locator);
injectComponent(fieldValue.getComponent(), container, componentSearchContext);
}
}
}
private boolean isLazyComponentsAndNotInitialized(Object component) {
if (component instanceof LazyComponents) {
LazyComponents lazyComponents = (LazyComponents) component;
return lazyComponents.isLazy() && !lazyComponents.isLazyInitialized();
}
return false;
}
private Hook getHookAnnotation(Annotation annotation) {
if (annotation instanceof Hook) {
return (Hook) annotation;
} else if (annotation.annotationType().isAnnotationPresent(Hook.class)) {
return annotation.annotationType().getAnnotation(Hook.class);
}
return null;
}
private HookOptions getHookOptionsAnnotation(Annotation annotation) {
if (annotation instanceof HookOptions) {
return (HookOptions) annotation;
} else if (annotation.annotationType().isAnnotationPresent(HookOptions.class)) {
return annotation.annotationType().getAnnotation(HookOptions.class);
}
return null;
}
private void addHookDefinitions(Annotation[] annotations, List<HookDefinition<?>> hookDefinitions) {
Hook currentHookAnnotation = null;
HookOptions currentHookOptionAnnotation = null;
Annotation currentAnnotation = null;
for (Annotation annotation : annotations) {
applyNoHook(hookDefinitions, annotation);
Hook hookAnnotation = getHookAnnotation(annotation);
if (hookAnnotation != null) {
currentAnnotation = annotation;
}
if (hookAnnotation != null && currentHookAnnotation != null) {
hookDefinitions.add(buildHookDefinition(currentHookAnnotation, currentHookOptionAnnotation, currentAnnotation));
currentHookAnnotation = null;
currentHookOptionAnnotation = null;
}
if (hookAnnotation != null) {
currentHookAnnotation = hookAnnotation;
}
HookOptions hookOptionsAnnotation = getHookOptionsAnnotation(annotation);
if (hookOptionsAnnotation != null) {
if (currentHookOptionAnnotation != null) {
throw new FluentInjectException("Unexpected @HookOptions annotation. @Hook is missing.");
}
currentHookOptionAnnotation = hookOptionsAnnotation;
}
}
if (currentHookAnnotation != null) {
hookDefinitions.add(buildHookDefinition(currentHookAnnotation, currentHookOptionAnnotation, currentAnnotation));
}
}
private void applyNoHook(List<HookDefinition<?>> hookDefinitions, Annotation annotation) {
if (annotation instanceof NoHook) {
Hook[] value = ((NoHook) annotation).value();
if (ArrayUtils.isEmpty(value)) {
hookDefinitions.clear();
} else {
List<? extends Class<? extends FluentHook<?>>> toRemove = Arrays.stream(value).map(Hook::value)
.collect(Collectors.toList());
HookControlImpl.removeHooksFromDefinitions(hookDefinitions, toRemove.toArray(new Class[toRemove.size()]));
}
}
}
private <T> HookDefinition<T> buildHookDefinition(Hook hookAnnotation, HookOptions hookOptionsAnnotation,
Annotation currentAnnotation) {
Class<? extends T> hookOptionsClass =
hookOptionsAnnotation == null ? null : (Class<? extends T>) hookOptionsAnnotation.value();
T fluentHookOptions = null;
if (hookOptionsClass != null) {
try {
fluentHookOptions = ReflectionUtils.newInstanceOptionalArgs(hookOptionsClass, currentAnnotation);
} catch (NoSuchMethodException e) {
throw new FluentInjectException("@HookOption class has no valid constructor", e);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
throw new FluentInjectException("Can't create @HookOption class instance", e);
}
}
Class<? extends FluentHook<T>> hookClass = (Class<? extends FluentHook<T>>) hookAnnotation.value();
if (fluentHookOptions == null) {
return new HookDefinition<>(hookClass);
}
return new HookDefinition<>(hookClass, fluentHookOptions);
}
private boolean isSupported(Object container, Field field) {
return isValueNull(container, field) && !field.isAnnotationPresent(NoInject.class) && !Modifier
.isFinal(field.getModifiers()) && (isListOfFluentWebElement(field) || isListOfComponent(field) || isComponent(
field) || isComponentList(field) || isElement(field) || isListOfElement(field));
}
private static boolean isValueNull(Object container, Field field) {
try {
return ReflectionUtils.get(field, container) == null;
} catch (IllegalAccessException e) {
throw new FluentInjectException("Can't retrieve default value of field", e);
}
}
private boolean isComponent(Field field) {
return componentsManager.isComponentClass(field.getType());
}
private boolean isComponentList(Field field) {
if (isList(field)) {
boolean componentListClass = componentsManager.isComponentListClass((Class<? extends List<?>>) field.getType());
if (componentListClass) {
Class<?> genericType = ReflectionUtils.getFirstGenericType(field);
boolean componentClass = componentsManager.isComponentClass(genericType);
if (componentClass) {
return true;
}
}
}
return false;
}
private static boolean isListOfFluentWebElement(Field field) {
if (isList(field)) {
Class<?> genericType = ReflectionUtils.getFirstGenericType(field);
return FluentWebElement.class.isAssignableFrom(genericType);
}
return false;
}
private boolean isListOfComponent(Field field) {
if (isList(field)) {
Class<?> genericType = ReflectionUtils.getFirstGenericType(field);
return componentsManager.isComponentClass(genericType);
}
return false;
}
private static boolean isList(Field field) {
return List.class.isAssignableFrom(field.getType());
}
private static boolean isElement(Field field) {
return WebElement.class.isAssignableFrom(field.getType());
}
private static boolean isListOfElement(Field field) {
if (isList(field)) {
Class<?> genericType = ReflectionUtils.getFirstGenericType(field);
return WebElement.class.isAssignableFrom(genericType);
}
return false;
}
@AllArgsConstructor
@Getter
private static class ComponentAndProxy<T, P> {
private T component;
private P proxy;
}
private ComponentAndProxy<?, ?> initFieldElements(ElementLocator locator, Field field) {
if (isComponent(field)) {
return initFieldAsComponent(locator, field);
} else if (isComponentList(field)) {
return initFieldAsComponentList(locator, field);
} else if (isListOfFluentWebElement(field)) {
return initFieldAsListOfFluentWebElement(locator, field);
} else if (isListOfComponent(field)) {
return initFieldAsListOfComponent(locator, field);
} else if (isElement(field)) {
return initFieldAsElement(locator);
} else if (isListOfElement(field)) {
return initFieldAsListOfElement(locator);
}
return null;
}
private <L extends List<T>, T> ComponentAndProxy<L, List<WebElement>> initFieldAsComponentList(ElementLocator locator,
Field field) {
List<WebElement> webElementList = LocatorProxies.createWebElementList(locator);
L componentList = componentsManager
.asComponentList((Class<L>) field.getType(), (Class<T>) ReflectionUtils.getFirstGenericType(field),
webElementList);
return new ComponentAndProxy<>(componentList, webElementList);
}
private ComponentAndProxy<Object, WebElement> initFieldAsComponent(ElementLocator locator, Field field) {
WebElement element = LocatorProxies.createWebElement(locator);
Object component = componentsManager.newComponent(field.getType(), element);
return new ComponentAndProxy(component, element);
}
private ComponentAndProxy<ComponentList<?>, List<WebElement>> initFieldAsListOfComponent(ElementLocator locator,
Field field) {
List<WebElement> webElementList = LocatorProxies.createWebElementList(locator);
ComponentList<?> componentList = componentsManager
.asComponentList(ReflectionUtils.getFirstGenericType(field), webElementList);
return new ComponentAndProxy(componentList, webElementList);
}
private ComponentAndProxy<FluentList<? extends FluentWebElement>, List<WebElement>> initFieldAsListOfFluentWebElement(
ElementLocator locator, Field field) {
List<WebElement> webElementList = LocatorProxies.createWebElementList(locator);
FluentList<? extends FluentWebElement> fluentList = componentsManager
.asFluentList((Class<? extends FluentWebElement>) ReflectionUtils.getFirstGenericType(field), webElementList);
return new ComponentAndProxy(fluentList, webElementList);
}
private ComponentAndProxy<WebElement, WebElement> initFieldAsElement(ElementLocator locator) {
WebElement element = LocatorProxies.createWebElement(locator);
return new ComponentAndProxy<>(element, element);
}
private ComponentAndProxy<List<WebElement>, List<WebElement>> initFieldAsListOfElement(ElementLocator locator) {
List<WebElement> elements = LocatorProxies.createWebElementList(locator);
return new ComponentAndProxy(elements, elements);
}
}