// Copyright 2013 SICK AG. All rights reserved.
package de.sick.guicheck.fx;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javafx.scene.Node;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.stage.Stage;
import de.sick.guicheck.GcAssertException;
import de.sick.guicheck.GcException;
import de.sick.guicheck.GcUtils;
import de.sick.guicheck.GcUtils.IEvaluator;
/**
* The base class for all wrappers around elements of the JavaFX UI. Not only {@link Node} is covert, also {@link Stage}
* and {@link Menu}. The child lookup methods again return wrappers for JavaFX {@link Node}.
* <p>
* This class supports the evaluation of property values using retries and idle waiting.
*
* @author linggol (created)
*/
abstract class GcComponentFX<T extends GcComponentFX<T>>
{
/**
* Used to convert a wrapper class to its corresponding primitive type.
*
* If this method is used at several places. Consider to use Guava or Apache commons-lang
*/
private final static Map<Class<?>, Class<?>> WRAPPER_TO_PRIMITIVE = new HashMap<Class<?>, Class<?>>();
static
{
WRAPPER_TO_PRIMITIVE.put(Boolean.class, boolean.class);
WRAPPER_TO_PRIMITIVE.put(Byte.class, byte.class);
WRAPPER_TO_PRIMITIVE.put(Short.class, short.class);
WRAPPER_TO_PRIMITIVE.put(Character.class, char.class);
WRAPPER_TO_PRIMITIVE.put(Integer.class, int.class);
WRAPPER_TO_PRIMITIVE.put(Long.class, long.class);
WRAPPER_TO_PRIMITIVE.put(Float.class, float.class);
WRAPPER_TO_PRIMITIVE.put(Double.class, double.class);
}
/**
* @return The JavaFX {@link Node} wrapped by this component.
*/
public abstract Node getNode();
/**
* @return The JavaFX component wrapped by this component. In most cases this is the same as the {@link Node}
* returned by {@link #getNode()}
*/
public abstract <Z> Z getFXComponent();
private Node findNode(final String selector)
{
return GcUtilsFX.eval(new IEvaluator<Node>()
{
@Override
public Node eval()
{
Node l_found = getNode().lookup(selector);
if (l_found != null)
{
return l_found;
}
throw new GcAssertException("Cannot find node for selector: " + selector);
}
});
}
private <TT> Node findNode(final String selector, final String property, final TT expected)
{
return GcUtilsFX.eval(new GcUtils.IEvaluator<Node>()
{
@SuppressWarnings("unchecked")
@Override
public Node eval()
{
Set<Node> l_nodes = getNode().lookupAll(selector);
for (Node l_found : l_nodes)
{
TT l_value;
try
{
// Get the property getter method ...
final Class<?> l_clazz = expected == null ? null : expected.getClass();
final Method l_method = l_found.getClass().getMethod(getPropertyGetter(property, l_clazz), (Class<?>[])null);
// ... get the value of the property ...
l_value = (TT)l_method.invoke(l_found, (Object[])null);
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e)
{
throw new GcException("Failed to access property " + property, e);
}
// ... and check it against the expected value
if (expected == l_value || (expected != null && expected.equals(l_value)))
{
return l_found;
}
}
throw new GcAssertException("Cannot find node for selector: " + selector + " with value " + expected + " for property " + property);
}
});
}
/**
* Find the first child {@link Node} for the given CSS selector.
*/
public GcNodeFX node(String selector)
{
return new GcNodeFX(findNode(selector));
}
/**
* Find the first child {@link Node} for the given CSS selector with the given value for the specified property.
*/
public <TT> GcNodeFX node(String selector, String property, TT expected)
{
return new GcNodeFX(findNode(selector, property, expected));
}
/**
* Find the first {@link MenuBar} for the given CSS selector.
*/
public GcMenuBarFX menuBar(String selector)
{
return new GcMenuBarFX((MenuBar)findNode(selector));
}
/**
* @return The name of the getter method for the given property using Java Bean style.
*/
static String getPropertyGetter(String property, Class<?> clazz)
{
StringBuffer l_sb = new StringBuffer((clazz == Boolean.class || clazz == boolean.class) ? "is" : "get");
if (property.length() > 0)
{
l_sb.append(property.substring(0, 1).toUpperCase());
if (property.length() > 1)
{
l_sb.append(property.substring(1));
}
}
return l_sb.toString();
}
/**
* @return The name of the setter method for the given property using Java Bean style.
*/
static String getPropertySetter(String property)
{
StringBuffer l_sb = new StringBuffer("set");
if (property.length() > 0)
{
l_sb.append(property.substring(0, 1).toUpperCase());
if (property.length() > 1)
{
l_sb.append(property.substring(1));
}
}
return l_sb.toString();
}
/**
* Duplicate method
*
* @see GcContextMenuFX#propertyIs(Object, String, Object, boolean)
*/
@SuppressWarnings("unchecked")
final <TT> T propertyIs(final Object obj, final String property, final TT value, final boolean expectedResult)
{
try
{
// Get the property getter method ...
final Class<?> l_clazz = value == null ? null : value.getClass();
final Method l_method = obj.getClass().getMethod(getPropertyGetter(property, l_clazz), (Class<?>[])null);
GcUtilsFX.eval(new GcUtils.IEvaluator<Void>()
{
@Override
public Void eval()
{
TT l_value;
try
{
// ... get the value of the property ...
l_value = (TT)l_method.invoke(obj, (Object[])null);
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
{
throw new GcException("Failed to access property " + property, e);
}
// ... and check it against the expected value
if ((value == l_value || (value != null && value.equals(l_value))) != expectedResult)
{
final StringBuilder l_sb = new StringBuilder("Unexpected value of ");
l_sb.append(property).append(": ").append(expectedResult ? "Expected: " : "Not expected: ").append(value).append(", Actual: ").append(l_value);
throw new GcAssertException(l_sb.toString());
}
return null;
}
});
// Return this instance again according to the fluent API style
return (T)this;
}
catch (NoSuchMethodException e)
{
throw new GcException("Failed to access property " + property, e);
}
}
/**
* Check if the given property has the given value. This method follows the fluent API style.
*/
public final <TT> T propertyIs(String property, TT value)
{
return propertyIs(getFXComponent(), property, value, true);
}
/**
* Check if the given property does not have the given value. This method follows the fluent API style.
*/
public final <TT> T propertyIsNot(String property, TT value)
{
return propertyIs(getFXComponent(), property, value, false);
}
@SuppressWarnings("unchecked")
private final <TT> T ensurePropertyIs(final Object obj, final String property, final TT value)
{
try
{
// Get the property setter method ...
final Method l_method = getSetter(obj, property, value);
GcUtilsFX.eval(new GcUtils.IEvaluator<Void>()
{
@Override
public Void eval()
{
try
{
// ... set the value of the property ...
l_method.invoke(obj, value);
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
{
throw new GcException("Failed to set property " + property, e);
}
// ... and check if it is set correct
propertyIs(property, value);
return null;
}
});
// Return this instance again according to the fluent API style
return (T)this;
}
catch (NoSuchMethodException e)
{
throw new GcException("Failed to set property " + property, e);
}
}
private <TT> Method getSetter(final Object obj, final String property, final TT value) throws NoSuchMethodException
{
final Class<?> l_clazz = value == null ? null : value.getClass();
try
{
return obj.getClass().getMethod(getPropertySetter(property), l_clazz);
}
catch (NoSuchMethodException e)
{
// try primitive type
return obj.getClass().getMethod(getPropertySetter(property), WRAPPER_TO_PRIMITIVE.get(l_clazz));
}
}
/**
* Set the value of the given property to the given value. The value will not be set via UI interaction (mouse click
* or key press). The property will be directly accessed via its setter. This method follows the fluent API style.
*/
public final <TT> T ensurePropertyIs(String property, TT value)
{
return ensurePropertyIs(getFXComponent(), property, value);
}
}