/*==========================================================================*\ | $Id: GUIFilter.java,v 1.5 2010/07/26 13:59:37 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2007-2010 Virginia Tech | | This file is part of the Student-Library. | | The Student-Library is free software; you can redistribute it and/or | modify it under the terms of the GNU Lesser General Public License as | published by the Free Software Foundation; either version 3 of the | License, or (at your option) any later version. | | The Student-Library is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU Lesser General Public License for more details. | | You should have received a copy of the GNU Lesser General Public License | along with the Student-Library; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package student.testingsupport; import java.awt.Component; import java.awt.Rectangle; import java.lang.reflect.Method; //------------------------------------------------------------------------- /** * This class Represents a filter or query that can be used to describe * a {@link Component} when searching. Note that the methods and fields * in this class are designed specifically to support a natural, readable, * boolean expression "mini-language" for use in describing a single * {@link Component} (or group of {@link Component}s) by its (or their) * properties. As a result, it does violate some conventions regarding * the use of public fields (although note that all here are immutable) * and occasionally even the naming conventions for constants (e.g., * <code>where</code>). However, breaking these conventions is necessary * in this class in order to support the more natural syntax for filter * expressions, and so was deemed a better design choice. * <p> * Client classes that wish to use these filters should add the * following static import directive: * </p> * <pre> * import static student.testingsupport.GUIFilter.ClientImports.*; * </pre> * <p> * Note that the {@link student.GUITestCase} class already re-exports the * items defined in the {@link ClientImports} nested class, so GUI test * cases should <em>not</em> include the static import. * </p> * <p> * The expressions that you can create with this class are designed to * represent "filters" or boolean predicates that can be applied to a * Component, returning true if the component "matches" the filter or false * if the component does not match. * </p> * <p> * Often, a filter object is created solely for the purpose of passing * the filter into some other operation, such as a search operation. For * example, the student.GUITestCase class provides a * {@link student.GUITestCase#getComponent(Class,GUIFilter) getComponent()} * method that takes a filter as a parameter. For the examples below, we * will use <code>getComponent()</code> as the context, specifying each * filter as an argument value in a call to that method. * </p> * <p> * The basic principles for using this class are as follows: * </p> * <ul> * <li><p>Never try to create a GUIFilter object directly. Instead, always * write something that looks like a boolean expression, and that * starts with the operator <code>where</code>:</p> * <pre> * JButton button = getComponent(JButton.class, where.nameIs("okButton")); * </pre></li> * <li><p>The basic properties you can check with filters include: * <code>nameIs()</code>, <code>textIs()</code>, * <code>enabledIs()</code>, <code>hasFocusIs()</code>, * <code>hasFocusIs()</code>, and <code>typeIs()</code>. They are * all used the same way:</p> * <pre> * JButton done = getComponent(JButton.class, where.textIs("Done")); * JLabel name = getComponent(JLabel.class, where.textIs("Name:")); * </pre></li> * <li><p>You can combine filters using logical "and" as necessary:</p> * <pre> * JButton done = getComponent(JButton.class, * where.nameIs("done").and.enabledIs(true).and.hasFocusIs(true)); * </pre></li> * <li><p>You can also use "or":</p> * <pre> * JButton done = getComponent(JButton.class, * where.nameIs("done").or.enabledIs(true).or.hasFocusIs(true)); * </pre></li> * <li><p>Operators like "and" and "or" are interpreted strictly left to * right. There is <b>no precedence</b>, because of the way Java interprets * dot notation.</p> * <pre> * JButton done = getComponent(JButton.class, * where.nameIs("done").or.enabledIs(true).and.hasFocusIs(true)); * // means ((name = "done" or enabled = true) and focus = true) * // note that the left operator is always evaluated first! * </pre> * <p>If you want to force a different order of evaluation * than strictly left-to-right, then use parentheses by writing the * appropriate operator as <code>and()</code> or <code>or()</code>. Just * be sure to start the new expression inside the parentheses with * <code>where</code>:</p> * <pre> * JButton done = getComponent(JButton.class, * where.nameIs("done").or(where.enabledIs(true).and.hasFocusIs(true))); * // now means (name = "done" or (enabled = true and focus = true)) * // because of the extra parentheses used * </pre></li> * <li><p>Finally, you can even use "not" (logical negation), but it is * called like a method, so parentheses (and thus a leading <code>where) * are <em>always required</em> to make the intended extent of the negation * clear:</p> * <pre> * JButton done = getComponent(JButton.class, * where.nameIs("done").and.not(where.enabledIs(true).or.hasFocusIs(true))); * </pre></li> * </ul> * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.5 $, $Date: 2010/07/26 13:59:37 $ */ public abstract class GUIFilter { //~ Instance/static variables ............................................. private String description; //~ Constructor ........................................................... // ---------------------------------------------------------- /** * Creates a new filter object. This constructor is not public, since * all filters are expected to be created using operators rather than * by calling new. * @param description A string description of this filter, used in * {@link #toString()}. */ protected GUIFilter(String description) { this.description = description; } //~ Public Fields ......................................................... // These fields are public to afford a more natural syntax, although they // can never be manipulated since they are final and have no mutators. // They are instance fields instead of static fields, because that is // necessary for their semantics. // ---------------------------------------------------------- /** * The "and" operator for combining filters, designed to be used in * expressions like <code>where.nameIs("...").and.enabledIs(true)</code>. * This operator is implemented as a public field so that the simple * <code>.and.</code> notation can be used as a connective between * filters. If you want to use parentheses for grouping to define * the right argument, see {@link #and(GUIFilter)} instead. */ public final BinaryOperator and = new BinaryOperator() { // ---------------------------------------------------------- @Override protected boolean combine(boolean leftResult, boolean rightResult) { return leftResult && rightResult; } // ---------------------------------------------------------- @Override protected String description( String leftDescription, String rightDescription) { return "(" + leftDescription + " AND " + rightDescription + ")"; } }; // ---------------------------------------------------------- /** * The "or" operator for combining filters, designed to be used in * expressions like <code>where.nameIs("abc").or.nameIs("def")</code>. * This operator is implemented as a public field so that the simple * <code>.or.</code> notation can be used as a connective between * filters. If you want to use parentheses for grouping to define * the right argument, see {@link #or(GUIFilter)} instead. */ public final BinaryOperator or = new BinaryOperator() { // ---------------------------------------------------------- @Override protected boolean combine(boolean leftResult, boolean rightResult) { return leftResult || rightResult; } // ---------------------------------------------------------- @Override protected String description( String leftDescription, String rightDescription) { return "(" + leftDescription + " OR " + rightDescription + ")"; } }; //~ Public Methods ........................................................ // ---------------------------------------------------------- /** * This base class represents an operator used to create a query. * As the base class for all operators, it defines the primitive * query operations supported for all {@link Component} objects, * each of which can be combined using any operator. */ public static abstract class Operator { // ---------------------------------------------------------- /** * Create a filter that compares the name of a component against a * given value. * @param name The name to look for * @return A new filter that succeeds only on components with the * given name */ public GUIFilter nameIs(final String name) { return applySelfTo(GUIFilter.nameIs(name)); } // ---------------------------------------------------------- /** * Create a filter that checks the text of a component by calling the * component's <code>getText()</code> method. * @param text The text to look for * @return A new filter that succeeds only on components where * <code>getText()</code> returns the specified text. */ public GUIFilter textIs(final String text) { return applySelfTo(GUIFilter.textIs(text)); } // ---------------------------------------------------------- /** * Create a filter that succeeds if a component has focus. * @param value True when searching for a component with focus, or * false when searching for one without. * @return A new filter that succeeds only on components that currently * has focus */ public GUIFilter hasFocusIs(final boolean value) { return applySelfTo(GUIFilter.hasFocusIs(value)); } // ---------------------------------------------------------- /** * Create a filter that succeeds if a component is enabled. * @param value True when searching for an enabled component, or false * when searching for a disabled component. * @return A new filter that succeeds only on components that currently * are enabled */ public GUIFilter enabledIs(final boolean value) { return applySelfTo(GUIFilter.enabledIs(value)); } // ---------------------------------------------------------- /** * Create a filter that checks the class of a component. * @param aClass The required class to check for (any subclass will * also match). * @return A new filter that only succeeds on instances of the * given class. */ public GUIFilter typeIs(final Class<? extends Component> aClass) { return applySelfTo(GUIFilter.typeIs(aClass)); } // ---------------------------------------------------------- /** * Create a filter that checks a component's width. * @param value The width to look for. * @return A new filter that succeeds only on components that have * the given width. */ public GUIFilter widthIs(final int value) { return applySelfTo(GUIFilter.widthIs(value)); } // ---------------------------------------------------------- /** * Create a filter that checks a component's height. * @param value The height to look for. * @return A new filter that succeeds only on components that have * the given height. */ public GUIFilter heightIs(final int value) { return applySelfTo(GUIFilter.heightIs(value)); } // ---------------------------------------------------------- /** * Create a filter that checks a component's size. * @param width The required width. * @param height The required height. * @return A new filter that succeeds only on components that have * the given size. */ public GUIFilter sizeIs(final int width, final int height) { return applySelfTo(GUIFilter.sizeIs(width, height)); } // ---------------------------------------------------------- /** * Create a filter that checks a component's size. * @param maxWidth The required width. * @param maxHeight The required height. * @return A new filter that succeeds only on components that have * the given size. */ public GUIFilter sizeIsWithin(final int maxWidth, final int maxHeight) { return applySelfTo(GUIFilter.sizeIsWithin(maxWidth, maxHeight)); } // ---------------------------------------------------------- /** * Create a filter that checks a component's x-coordinate. * @param x The required x-coordinate, relative to the * component's parent. * @return A new filter that succeeds only on components that have * the given x-coordinate. */ public GUIFilter xLocationIs(final int x) { return applySelfTo(GUIFilter.xLocationIs(x)); } // ---------------------------------------------------------- /** * Create a filter that checks a component's y-coordinate. * @param y The required y-coordinate, relative to the * component's parent. * @return A new filter that succeeds only on components that have * the given y-coordinate. */ public GUIFilter yLocationIs(final int y) { return applySelfTo(GUIFilter.yLocationIs(y)); } // ---------------------------------------------------------- /** * Create a filter that checks a component's location relative to * its parent. * @param x The required x-coordinate, relative to the * component's parent. * @param y The required y-coordinate, relative to the * component's parent. * @return A new filter that succeeds only on components that have * the given location. */ public GUIFilter locationIs(final int x, final int y) { return applySelfTo(GUIFilter.locationIs(x, y)); } // ---------------------------------------------------------- /** * Create a filter that checks whether a component's location (its * top left corner) lies within a specific rectangle. * @param region A rectangle defining a region in the component's * parent. * @return A new filter that succeeds only on components that have * a location within the given region. */ public GUIFilter isLocatedWithin(final Rectangle region) { return applySelfTo(GUIFilter.isLocatedWithin(region)); } // ---------------------------------------------------------- /** * Create a filter that checks whether a component's bounding box * (as returned by {@link Component#getBounds()}) lies within a * specific rectangle--that is, whether the entire component's area, * rather than just its top left corner, lies within the specified * region. * @param region A rectangle defining a region in the component's * parent. * @return A new filter that succeeds only on components that * lie entirely within the given region, as determined by * {@link Rectangle#contains(Rectangle)}. */ public GUIFilter isContainedWithin(final Rectangle region) { return applySelfTo(GUIFilter.isContainedWithin(region)); } /** * Create a filter that checks a component's parent * @param parent the parent component of the component being checked * @return A new filter that succeeds only on components that * are children of parent * */ public GUIFilter parentIs(final Component parent) { return applySelfTo(GUIFilter.parentIs(parent)); } /** * Create a filter that checks a component's ancestor * @param ancestor one of the ancestors of the component being checked * @return A new filter that succeeds only on components that * are descendants of ancestor */ public GUIFilter ancestorIs(final Component ancestor) { return applySelfTo(GUIFilter.ancestorIs(ancestor)); } // ---------------------------------------------------------- /** * Concrete subclasses must override this to implement an * operation on the filter being passed in to transform it into * another filter. * @param otherFilter The argument to transform (second argument, * for binary operators) * @return A new compound filter that includes the given argument * as one subfilter, after applying this operator to it. */ protected abstract GUIFilter applySelfTo(final GUIFilter otherFilter); } // ---------------------------------------------------------- /** * A non-static subclass for binary operators that implicitly * captures the outer filter to which it belongs, using it as * the first/left argument to the operator. */ public abstract class BinaryOperator extends Operator { // ---------------------------------------------------------- /** * The "not" operator for negating an existing filter, when you * want to use parentheses to group its righthand argument. This * method is designed to be used in expressions like * <code>where.nameIs("abc").and.not(enabledIs(true).or.hasFocusIs(true))</code>. * If you wish to use the <code>.not.</code> notation instead, leaving * off the parentheses, see {@link BinaryOperator#not}. * * @param otherFilter The filter to negate * @return A new filter that represents a combination of the left * filter with "NOT otherFilter". */ public GUIFilter not(final GUIFilter otherFilter) { return applySelfTo(primitiveNot(otherFilter)); } // ---------------------------------------------------------- /** * Implements a composite filter based on a binary operation, * where the "left"/"first" filter is the parent from which this * class was created, and the "right"/"second" filter is the * argument supplied to this operation. * @param otherFilter The argument to transform (second argument, * for binary operators) * @return A new compound filter that represents a combination * of the first and second filters. */ @Override protected GUIFilter applySelfTo(final GUIFilter otherFilter) { return new GUIFilter(description( GUIFilter.this.toString(), otherFilter.toString())) { public boolean test(Component component) { return combine(GUIFilter.this.test(component), otherFilter.test(component)); } }; } // ---------------------------------------------------------- /** * Concrete subclasses must override this to implement the * appropriate logic for combining the results of the two filters * being combined. * @param leftResult The boolean result of the left filter * @param rightResult The boolean result of the right filter * @return The result of this combined filter. */ protected abstract boolean combine( boolean leftResult, boolean rightResult); // ---------------------------------------------------------- /** * Concrete subclasses must override this to implement the * appropriate logic for building a description of this filter * based on the descriptions of the two filters * being combined. * @param leftDescription The description of the left filter * @param rightDescription The description of the right filter * @return The description of this combined filter. */ protected abstract String description( String leftDescription, String rightDescription); } // ---------------------------------------------------------- /** * Get a string representation of this filter. * @return A string representation of this filter. */ public String toString() { return description; } // ---------------------------------------------------------- /** * The "and" operator for combining filters, when you want to use * parentheses to group its righthand argument. This method is designed * to be used in expressions like * <code>where.nameIs("abc").and(enabledIs(true).or.hasFocusIs(true))</code>. * If you wish to use the <code>.and.</code> notation instead, leaving * off the parentheses, see {@link BinaryOperator#and(GUIFilter)}. * * @param otherFilter The second argument to "and". * @return A new filter object that represents "this AND otherFilter". */ public final GUIFilter and(final GUIFilter otherFilter) { final GUIFilter self = this; GUIFilter gf = new GUIFilter("(" + this + " AND " + otherFilter + ")") { public boolean test(Component component) { return self.test(component) && otherFilter.test(component); } }; return gf; } // ---------------------------------------------------------- /** * The "or" operator for combining filters, when you want to use * parentheses to group its righthand argument. This method is designed * to be used in expressions like * <code>where.nameIs("abc").or(enabledIs(true).and.hasFocusIs(true))</code>. * If you wish to use the <code>.or.</code> notation instead, leaving * off the parentheses, see {@link BinaryOperator#or(GUIFilter)}. * * @param otherFilter The second argument to "or". * @return A new filter object that represents "this OR otherFilter". */ public final GUIFilter or(final GUIFilter otherFilter) { final GUIFilter self = this; GUIFilter gf = new GUIFilter("(" + this + " OR " + otherFilter + ")") { public boolean test(Component component) { return self.test(component) || otherFilter.test(component); } }; return gf; } // ---------------------------------------------------------- /** * Evaluate whether a component matches this filter. This operation is * intended to be overridden by each subclass to implement the actual * check that a specific kind of filter performs. * @param component The component to check * @return true if the component matches this filter */ public abstract boolean test(Component component); // ---------------------------------------------------------- /** * This class represents the "where" operator that is used to begin * a filter expression. Client classes that wish to support filter * syntax should declare a final field (static or instance) like * this: * <pre> * public static final GUIFilter.WhereOperator where = * new GUIFilter.WhereOperator(); * </pre> */ public static class ClientImports { // ---------------------------------------------------------- /** * This object represents the "where" operator that is used to begin * a filter expression. */ public static final Operator where = new Operator() { // ---------------------------------------------------------- @Override protected GUIFilter applySelfTo(GUIFilter filter) { return filter; } }; // ---------------------------------------------------------- /** * The "not" operator for negating an existing filter, when the not * operation is at the very beginning of the expression. This * method is designed to be used in expressions like * <code>not(where.enabledIs(true).or.hasFocusIs(true))</code>. * * @param otherFilter The filter to negate * @return A new filter that represents a combination of the left * filter with "NOT otherFilter". */ public static GUIFilter not(final GUIFilter otherFilter) { return primitiveNot(otherFilter); } } //~ Private Methods/Declarations .......................................... // ---------------------------------------------------------- private static GUIFilter nameIs(final String name) { GUIFilter gf = new GUIFilter("name = \"" + name + "\"") { public boolean test(Component component) { if (component.getName() == null) { return name == null; } else { return component.getName().equals(name); } } }; return gf; } // ---------------------------------------------------------- private static GUIFilter textIs(final String text) { GUIFilter gf = new GUIFilter("text = \"" + text + "\"") { public boolean test(Component component) { Method m = null; try { m = component.getClass().getMethod("getText"); return ((String)m.invoke(component)).equals(text); } catch (Exception e) { return false; } } }; return gf; } // ---------------------------------------------------------- private static GUIFilter hasFocusIs(final boolean value) { GUIFilter gf = new GUIFilter("hasFocus = " + value) { public boolean test(Component component) { return component.isFocusOwner() == value; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter enabledIs(final boolean value) { GUIFilter gf = new GUIFilter("enabled = " + value) { public boolean test(Component component) { return component.isEnabled() == value; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter widthIs(final int value) { GUIFilter gf = new GUIFilter("width = " + value) { public boolean test(Component component) { return component.getWidth() == value; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter heightIs(final int value) { GUIFilter gf = new GUIFilter("height = " + value) { public boolean test(Component component) { return component.getHeight() == value; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter sizeIs(final int width, final int height) { GUIFilter gf = new GUIFilter("size = (" + width + ", " + height + ")") { public boolean test(Component component) { return component.getWidth() == width && component.getHeight() == height; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter sizeIsWithin( final int maxWidth, final int maxHeight) { GUIFilter gf = new GUIFilter( "sizeIsWithin(" + maxWidth + ", " + maxHeight + ")") { public boolean test(Component component) { return component.getWidth() <= maxWidth && component.getHeight() <= maxHeight; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter xLocationIs(final int value) { GUIFilter gf = new GUIFilter("xLocation = " + value) { public boolean test(Component component) { return component.getX() == value; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter yLocationIs(final int value) { GUIFilter gf = new GUIFilter("yLocation = " + value) { public boolean test(Component component) { return component.getY() == value; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter locationIs(final int x, final int y) { GUIFilter gf = new GUIFilter("location = (" + x + ", " + y + ")") { public boolean test(Component component) { return component.getX() == x && component.getY() == y; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter isLocatedWithin(final Rectangle region) { GUIFilter gf = new GUIFilter("isLocatedWithin(" + region + ")") { public boolean test(Component component) { return region.contains(component.getLocation()); } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter isContainedWithin(final Rectangle region) { GUIFilter gf = new GUIFilter("isContainedWithin(" + region + ")") { public boolean test(Component component) { return region.contains(component.getBounds()); } }; return gf; } // ---------------------------------------------------------- private static GUIFilter typeIs(final Class<? extends Component> aClass) { GUIFilter gf = new GUIFilter("type = " + aClass.getSimpleName()) { public boolean test(Component component) { return aClass.isAssignableFrom(component.getClass()); } }; return gf; } // ---------------------------------------------------------- private static GUIFilter parentIs(final Component parent) { GUIFilter gf = new GUIFilter("parent is " + parent) { public boolean test(Component component) { return component.getParent() == parent; } }; return gf; } // ---------------------------------------------------------- private static GUIFilter ancestorIs(final Component ancestor) { GUIFilter gf = new GUIFilter("ancestor is " + ancestor) { public boolean test(Component component) { Component c = component; while (c != null && c != ancestor) { c = c.getParent(); } return c != null; } }; return gf; } // ---------------------------------------------------------- private static final GUIFilter primitiveNot(final GUIFilter otherFilter) { return new GUIFilter("(NOT " + otherFilter + ")") { public boolean test(Component component) { return !otherFilter.test(component); } }; } }