/******************************************************************************* * Copyright (c) 2013 BREDEX GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * BREDEX GmbH - initial API and implementation and/or initial documentation *******************************************************************************/ package org.eclipse.jubula.rc.javafx.listener; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jubula.rc.common.AUTServerConfiguration; import org.eclipse.jubula.rc.common.adaptable.AdapterFactoryRegistry; import org.eclipse.jubula.rc.common.components.AUTComponent; import org.eclipse.jubula.rc.common.exception.ComponentNotFoundException; import org.eclipse.jubula.rc.common.exception.ComponentNotManagedException; import org.eclipse.jubula.rc.common.exception.NoIdentifierForComponentException; import org.eclipse.jubula.rc.common.listener.BaseAUTListener; import org.eclipse.jubula.rc.common.logger.AutServerLogger; import org.eclipse.jubula.rc.javafx.components.AUTJavaFXHierarchy; import org.eclipse.jubula.rc.javafx.components.CurrentStages; import org.eclipse.jubula.rc.javafx.components.FindJavaFXComponentBP; import org.eclipse.jubula.rc.javafx.components.JavaFXComponent; import org.eclipse.jubula.rc.javafx.driver.EventThreadQueuerJavaFXImpl; import org.eclipse.jubula.rc.javafx.listener.sync.IStageResizeSync; import org.eclipse.jubula.rc.javafx.listener.sync.StageResizeSyncFactory; import org.eclipse.jubula.rc.javafx.tester.adapter.IContainerAdapter; import org.eclipse.jubula.rc.javafx.tester.util.NodeBounds; import org.eclipse.jubula.rc.javafx.tester.util.NodeTraverseHelper; import org.eclipse.jubula.tools.internal.constants.TimingConstantsServer; import org.eclipse.jubula.tools.internal.exception.InvalidDataException; import org.eclipse.jubula.tools.internal.messagehandling.MessageIDs; import org.eclipse.jubula.tools.internal.objects.IComponentIdentifier; import org.eclipse.jubula.tools.internal.utils.TimeUtil; import org.eclipse.jubula.tools.internal.xml.businessmodell.ComponentClass; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.event.EventTarget; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.Skin; import javafx.scene.control.SkinBase; import javafx.scene.control.Skinnable; import javafx.stage.PopupWindow; import javafx.stage.Window; import javafx.stage.WindowEvent; /** * This class is responsible for handling the components of the AUT. <br> * * The static methods for fetching an identifier for a component and getting the * component for an identifier delegates to this AUTHierarchy. * * @author BREDEX GmbH * @created 10.10.2013 */ public class ComponentHandler implements ListChangeListener<Window>, BaseAUTListener { /** the logger */ private static AutServerLogger log = new AutServerLogger( ComponentHandler.class); /** the Container hierarchy of the AUT */ private static AUTJavaFXHierarchy hierarchy = new AUTJavaFXHierarchy(); /** Businessprocess for getting components */ private static FindJavaFXComponentBP findBP = new FindJavaFXComponentBP(); /** used for synchronizing on stage resize events */ private static IStageResizeSync stageResizeSync = StageResizeSyncFactory.instance(); /**lock for hierarchy access**/ private static volatile ReentrantLock lock = AUTJavaFXHierarchy.getLock(); /** * Constructor. Adds itself as ListChangeListener to the Stages-List */ public ComponentHandler() { CurrentStages.addStagesListener(this); } @Override public void onChanged(Change<? extends Window> change) { change.next(); for (final Window win : change.getRemoved()) { hierarchy.removeComponentFromHierarchy(win); } for (final Window win : change.getAddedSubList()) { if (win.isShowing()) { hierarchy.createHierarchyFrom(win); stageResizeSync.register(win); } else { win.addEventFilter(WindowEvent.WINDOW_SHOWN, new EventHandler<WindowEvent>() { @Override public void handle(WindowEvent event) { hierarchy.createHierarchyFrom(win); stageResizeSync.register(win); win.removeEventFilter(WindowEvent.WINDOW_SHOWN, this); } }); } } } @Override public long[] getEventMask() { return null; } /** * @return the Container hierarchy of the AUT */ public static AUTJavaFXHierarchy getAutHierarchy() { return hierarchy; } /** * Searches the hierarchy-map (in the JavaFX-Thread) for components that are * assignable from the given type * * @param type the type to look for * @param <T> component type * @return List */ public static <T> List<? extends T> getAssignableFrom(final Class<T> type) { return EventThreadQueuerJavaFXImpl.invokeAndWait("getAssignableFrom", //$NON-NLS-1$ new Callable<List<? extends T>>() { @Override public List<? extends T> call() throws Exception { Set<JavaFXComponent> keys = (Set<JavaFXComponent>) hierarchy.getHierarchyMap().keySet(); List<T> result = new ArrayList<T>(); for (AUTComponent<EventTarget> object : keys) { EventTarget component = object.getComponent(); if (type.isAssignableFrom(component.getClass())) { result.add(type.cast(component)); } } return result; } }); } /** * Traverses the scene graph from the given parent and adds all nodes to a * result list if the following conditions are met: The node is visible and * the given position is within the bounds of this node * * @param parent * the parent * @param pos * the position * @param resultList * the result list * @return A list witch all nodes which are visible and the given position is * within the bounds of this node */ private static List<Node> getAllNodesforPos(Parent parent, Point2D pos, List<Node> resultList) { //Blame checkstyle for this extra list List<Node> result = resultList; if (parent.isVisible()) { for (Node child : parent.getChildrenUnmodifiable()) { if (child.isVisible() && NodeBounds.checkIfContains(pos, child)) { result.add(child); if (child instanceof Parent) { result = getAllNodesforPos((Parent) child, pos, result); } } } } return result; } /** * Returns the node under the given point * * @param pos * the point * @return the component */ public static Node getComponentByPos(Point2D pos) { List<? extends Window> comps = getAssignableFrom(Window.class); Map<Window, List<Node>> matchesByWindows = new HashMap<Window, List<Node>>(); Set<Window> shadowedWindows = new HashSet<>(); List<Node> localNodes; for (Window window : comps) { localNodes = new ArrayList<Node>(); matchesByWindows.put(window, localNodes); if ((window.isFocused() && window.isShowing()) || (window.isShowing() && window instanceof PopupWindow)) { Parent root = window.getScene().getRoot(); localNodes = getAllNodesforPos(root, pos, localNodes); if (!localNodes.isEmpty() && window instanceof PopupWindow) { // if a popup window is under the cursor, it shadows its owner window // we need this, because the owner window is both visible and has a focus shadowedWindows.add(((PopupWindow) window). getOwnerWindow()); } } } List<Node> matches = new ArrayList<Node>(); for (Window window : matchesByWindows.keySet()) { // we only keep not-shadowed window components if (!shadowedWindows.contains(window)) { matches.addAll(matchesByWindows.get(window)); } } List<Node> result = new ArrayList<Node>(); for (Node n : matches) { if (isMappable(n)) { result.add(n); } } if (result.size() == 0) { return null; } if (result.size() == 1) { return result.get(0); } // multiple matches, try filtering return filterMatches(result); } /** * Determines whether a node is mappable or not * @param n the node * @return the result */ public static boolean isMappable(Node n) { boolean mappable = true; if (n.getScene() == null || !isSupported(n.getClass()) || !n.isVisible()) { return false; } Parent parent = n.getParent(); while (parent != null) { if (parent instanceof Skinnable || isContainer(parent)) { if (isContentNode(n, parent)) { break; } Skin<?> skin = ((Skinnable) parent).getSkin(); if (skin instanceof SkinBase) { // We don't want skin nodes if (isSkinNode(n, (SkinBase<?>) skin)) { mappable = false; break; } } else { parent = parent.getParent(); } } else { parent = parent.getParent(); } } return mappable; } /** * Checks if the given node is a container. * @param n the possible container * @return true if the node is a container false otherwise */ private static boolean isContainer(Node n) { IContainerAdapter adapter = (IContainerAdapter) AdapterFactoryRegistry. getInstance().getAdapter(IContainerAdapter.class, n); return adapter != null; } /** * Checks if the given class is supported by the AUT-Server * @param c the class * @return true if the type is supported, false if not */ private static boolean isSupported(Class<?> c) { Set<ComponentClass> supportedTypes = AUTServerConfiguration .getInstance().getSupportedTypes(); Class<?> currentClass = c; while (currentClass != null) { for (Object object : supportedTypes) { if (((ComponentClass) object).getName().equals( currentClass.getName())) { return true; } } currentClass = currentClass.getSuperclass(); } return false; } /** * Checks if the given Node is Part of the Content of the given Parent. * This is checked for the following types: * TitledPane * ScrollPane * SplitPane * ToolBar * This Classes don't share a parent class which would make accessing * the Content easier. Therefore this special (bad) code is necessary. * @param n the possible content node. * @param parent a parent of the above mentioned type * @return true if the given node is part of the content, false if not. */ private static boolean isContentNode(Node n, Parent parent) { IContainerAdapter adapter = (IContainerAdapter) AdapterFactoryRegistry. getInstance().getAdapter(IContainerAdapter.class, parent); if (adapter != null) { List<? extends Node> content = adapter.getContent(); if (content.contains(n)) { return true; } for (Node contentNode : content) { if (contentNode instanceof Parent && NodeTraverseHelper.isChildOf( n, (Parent)contentNode)) { return true; } } } return false; } /** * Checks if the given node is part of a skin * @param node the node * @param skin the skin * @return true if it is part the given skin, false if not */ private static boolean isSkinNode(Node node, SkinBase<?> skin) { ObservableList<Node> skinChildren = skin.getChildren(); for (Node n : skinChildren) { if (n == node) { return true; } else if (n instanceof Parent) { if (NodeTraverseHelper.isChildOf(node, (Parent) n)) { return true; } } } return false; } /** * Filters out all parent in a list of matches * @param matches the matches * @return the filtered list */ private static Node filterMatches(List<Node> matches) { List<Node> filteredMatches = filterOutUnfocussedNodes(matches); if (filteredMatches.size() == 1) { return filteredMatches.get(0); } Node firstCommonAncestor = findFirstCommonAncestor(filteredMatches); /* Always of type Parent */ if (firstCommonAncestor != null) { return topMostDescendant( (Parent)firstCommonAncestor, filteredMatches); } return null; } /** * Filters out nodes from unfocused windows from a given list * @param matches the list * @return list containing only the nodes of focused window */ private static List<Node> filterOutUnfocussedNodes(List<Node> matches) { List<Node> filteredMatches = new ArrayList<Node>(); for (Node match : matches) { if (match.getScene().getWindow().isFocused()) { filteredMatches.add(match); } } return filteredMatches; } /** * Returns all instances of the type Node from a given list which are * descendants of a given parent node * @param parent the parent * @param matches list of possible descendants * @return all descendants of the parent which also occur in the list */ private static List<Node> filterDescendants(Parent parent, List<Node> matches) { List<Node> descendants = new ArrayList<Node>(); for (Node match : matches) { if (isDescendant(match, parent)) { descendants.add(match); } } return descendants; } /** * Returns the descendant of a parent node from a given list which has * the highest "z-coordinate". * @param parent the parent * @param matches the list of descendants * @return the top-most descendant from the list */ private static Node topMostDescendant(Parent parent, List<Node> matches) { ObservableList<Node> children = parent.getChildrenUnmodifiable(); ArrayList<Node> revertedChildren = new ArrayList<Node>(children); Collections.reverse(revertedChildren); // Start by checking if the last child of the StackPane is among the matches for (Node child : revertedChildren) { if (child instanceof Parent) { List<Node> remainingMatches = filterDescendants((Parent)child, matches); if (remainingMatches.size() == 1) { return remainingMatches.get(0); } else if (remainingMatches.size() > 1) { return topMostDescendant( ((Parent) child), remainingMatches); } } if (matches.contains(child)) { return child; } } return null; } /** * Checks whether a node is a descendant of a node * @param candidate the possible descendant * @param node the node * @return whether the candidate is a descendant of the other node */ private static boolean isDescendant(Node candidate, Node node) { if (candidate == null) { return false; } else if (candidate == node) { return true; } return isDescendant(candidate.getParent(), node); } /** * Finds the first common ancestor of a list of nodes * @param nodelist the list * @return the first common ancestor */ private static Node findFirstCommonAncestor(List<Node> nodelist) { if (nodelist == null || nodelist.size() <= 0) { return null; } else if (nodelist.size() == 1) { return nodelist.get(0); } else { return findFirstCommonAncestor(nodelist.get(0), findFirstCommonAncestor(nodelist.subList( 1, nodelist.size()))); } } /** * Finds the first common ancestor of two nodes * @param node1 first node * @param node2 second node * @return their first common ancestor */ private static Node findFirstCommonAncestor(Node node1, Node node2) { if (node1 == null || node2 == null) { return null; } if (isDescendant(node1, node2)) { return node2; } return findFirstCommonAncestor(node1, node2.getParent()); } /** * Investigates the given <code>component</code> for an identifier. It must * be distinct for the whole AUT. To obtain this identifier the AUTHierarchy * is queried. * * @param node * the node to get an identifier for * @throws NoIdentifierForComponentException * if an identifier could not created for <code>component</code> * . * @return the identifier, containing the identification */ public static IComponentIdentifier getIdentifier(Node node) throws NoIdentifierForComponentException { try { return hierarchy.getComponentIdentifier(node); } catch (ComponentNotManagedException cnme) { log.warn(cnme); throw new NoIdentifierForComponentException( "unable to create an identifier for '" //$NON-NLS-1$ + node + "'", //$NON-NLS-1$ MessageIDs.E_COMPONENT_ID_CREATION); } } /** * Finds a Node by id * * @param id * the id * @return the node ore null if there is nothing or something else than a * node found */ public static Node findNodeByID(IComponentIdentifier id) { Object comp = findBP.findComponent(id, hierarchy); if (comp != null && comp instanceof Node) { return (Node) comp; } return null; } /** * Searches the component in the AUT, which belongs to the given * <code>componentIdentifier</code>. * * @param componentIdentifier * the identifier of the component to search for * @param retry * number of tries to get object * @param timeout * timeout for retries * @throws ComponentNotFoundException * if no component is found for the given identifier. * @throws IllegalArgumentException * if the identifier is null or contains invalid data * {@inheritDoc} * @return the found component */ public static Object findComponent( IComponentIdentifier componentIdentifier, boolean retry, int timeout) throws ComponentNotFoundException, IllegalArgumentException { long start = System.currentTimeMillis(); try { lock.lock(); return hierarchy.findComponent(componentIdentifier); } catch (ComponentNotManagedException cnme) { if (retry) { if (lock.isHeldByCurrentThread()) { lock.unlock(); } while (System.currentTimeMillis() - start < timeout) { try { lock.lock(); return hierarchy.findComponent(componentIdentifier); } catch (ComponentNotManagedException e) { // NOPMD by zeb // on 10.04.07 // 15:25 // OK, we will throw a corresponding exception later // if we really can't find the component if (lock.isHeldByCurrentThread()) { lock.unlock(); } try { Thread.sleep(TimingConstantsServer. POLLING_DELAY_FIND_COMPONENT); } catch (InterruptedException e1) { // ok } } catch (InvalidDataException ide) { // NOPMD by zeb on // 10.04.07 15:25 // OK, we will throw a corresponding exception later // if we really can't find the component } } } throw new ComponentNotFoundException(cnme.getMessage(), MessageIDs.E_COMPONENT_NOT_FOUND); } catch (IllegalArgumentException iae) { log.error(iae); throw iae; } catch (InvalidDataException ide) { log.error(ide); throw new ComponentNotFoundException(ide.getMessage(), MessageIDs.E_COMPONENT_NOT_FOUND); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } /** * Checks the component in the AUT, which belongs to the given * <code>componentIdentifier</code> is disappeared or nor. * * @param componentIdentifier * the identifier of the component to search for * @param timeout * timeout for retry * @throws ComponentNotFoundException * if no component is found for the given identifier. * @throws IllegalArgumentException * if the identifier is null or contains invalid data * {@inheritDoc} * @return true if the component is disappeared else false */ public static boolean isComponentDisappeared( IComponentIdentifier componentIdentifier, int timeout) throws ComponentNotFoundException, IllegalArgumentException { long start = System.currentTimeMillis(); try { lock.lock(); EventTarget component = (EventTarget) hierarchy .findComponent(componentIdentifier); if (lock.isHeldByCurrentThread()) { lock.unlock(); } while (System.currentTimeMillis() - start < timeout) { lock.lock(); TimeUtil.delay( TimingConstantsServer.POLLING_DELAY_FIND_COMPONENT); boolean isComponentDisappeared = !hierarchy .isComponentInHierarchy(component); if (lock.isHeldByCurrentThread()) { lock.unlock(); } if (isComponentDisappeared) { return true; } } return false; } catch (ComponentNotManagedException cnme) { return true; } catch (IllegalArgumentException iae) { log.error(iae); throw iae; } catch (InvalidDataException ide) { log.error(ide); throw new ComponentNotFoundException(ide.getMessage(), MessageIDs.E_COMPONENT_NOT_FOUND); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } /** * Blocks the calling thread until the Stage has been sufficiently resized * to deliver reliable component bounds. May not be called on the FX Thread. */ public static void syncStageResize() { stageResizeSync.await(); } }