/*==========================================================================*\ | $Id: ObjectdrawFilter.java,v 1.3 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.Color; import java.awt.Rectangle; import java.lang.reflect.Method; import objectdraw.Drawable1DInterface; import objectdraw.Drawable2DInterface; import objectdraw.DrawableInterface; import objectdraw.Location; //------------------------------------------------------------------------- /** * This class Represents a filter or query that can be used to describe * a {@link DrawableInterface} 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 DrawableInterface} (or group of {@link DrawableInterface}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.ObjectdrawFilter.ClientImports.*; * </pre> * <p> * Note that the {@link student.ObjectdrawTestCase} class already re-exports the * items defined in the {@link ClientImports} nested class, so Objectdraw 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 * DrawableInterface, returning true if the object "matches" the filter or false * if the object 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.ODTestCase class provides a * {@link student.ObjectdrawTestCase#getDrawable(Class,ObjectdrawFilter) getDrawable()} * method that takes a filter as a parameter. For the examples below, we * will use <code>getDrawable()</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 ODFilter object directly. Instead, always * write something that looks like a boolean expression, and that * starts with the operator <code>where</code>:</p> * <pre> * FilledRect rect = getDrawable(FilledRect.class, where.heightIs(10)); * </pre></li> * <li><p>The basic properties you can check with filters include: * <code>sizeIs</code>, <code>textIs()</code>, * <code>locationIs()</code>, <code>hiddenIs()</code>, * and <code>typeIs()</code>. They are * all used the same way:</p> * <pre> * FilledRect rect = getDrawable(FilledRect.class, where.widthIs(10); * FramedOval oval = getDrawable(FramedOval.class, where.hiddenIs(true)); * </pre></li> * <li><p>You can combine filters using logical "and" as necessary:</p> * <pre> * FilledRect rect = getDrawable(FilledRect.class, * where.heightIs(10).and.widthIs(20).and.hiddenIs(false)); * </pre></li> * <li><p>You can also use "or":</p> * <pre> * FilledRect rect = getDrawable(FilledRect.class, * where.heightIs(10).or.widthIs(20).or.hiddenIs(false)); * </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> * FilledRect rect = getDrawable(FilledRect.class, * where.heightIs(10).or.widthIs(20).and.hiddenIs(false)); * // means ((height = 10 or width = 20) and hidden = false) * // 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> * FilledRect rect = getDrawable(FilledRect.class, * where.heightIs(10).or(where.widthIs(20).and.hiddenIs(false))); * // now means (height = 10 or (width = 20 and hidden = false)) * // 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> * FilledRect rect = getDrawable(FilledRect.class, * where.heightIs(10).and.not(where.widthIs(20).or.hiddenIs(true))); * </pre></li> * </ul> * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.3 $, $Date: 2010/07/26 13:59:37 $ */ public abstract class ObjectdrawFilter { //~ 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 ObjectdrawFilter(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.heightIs(10).and.hiddenIs(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(ObjectdrawFilter)} 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(ObjectdrawFilter)} 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 DrawableInterface} objects, * each of which can be combined using any operator. */ public static abstract class Operator { // ---------------------------------------------------------- /** * Create a filter that checks the text of a shape by calling the * shape's <code>getText()</code> method, if it has one. * @param text The text to look for * @return A new filter that succeeds only on shape where * <code>getText()</code> is a valid method and * returns the specified text. */ public ObjectdrawFilter textIs(final String text) { return applySelfTo(ObjectdrawFilter.textIs(text)); } // ---------------------------------------------------------- /** * Create a filter that checks the class of a shape. * @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 ObjectdrawFilter typeIs( final Class<? extends DrawableInterface> aClass) { return applySelfTo(ObjectdrawFilter.typeIs(aClass)); } // ---------------------------------------------------------- /** * Create a filter that checks a shape's width. * @param value The width to look for. * @return A new filter that succeeds only on shapes that have * the given width. */ public ObjectdrawFilter widthIs(final int value) { return applySelfTo(ObjectdrawFilter.widthIs(value)); } // ---------------------------------------------------------- /** * Create a filter that checks a shape's height. * @param value The height to look for. * @return A new filter that succeeds only on shapes that have * the given height. */ public ObjectdrawFilter heightIs(final int value) { return applySelfTo(ObjectdrawFilter.heightIs(value)); } // ---------------------------------------------------------- /** * Create a filter that checks a shape's size. * @param width The required width. * @param height The required height. * @return A new filter that succeeds only on shapes that have * the given size. */ public ObjectdrawFilter sizeIs(final int width, final int height) { return applySelfTo(ObjectdrawFilter.sizeIs(width, height)); } // ---------------------------------------------------------- /** * Create a filter that checks a shape's size. * @param maxWidth The required width. * @param maxHeight The required height. * @return A new filter that succeeds only on shapes that have * the given size. */ public ObjectdrawFilter sizeIsWithin( final int maxWidth, final int maxHeight) { return applySelfTo( ObjectdrawFilter.sizeIsWithin(maxWidth, maxHeight)); } // ---------------------------------------------------------- /** * Create a filter that checks a shape's x-coordinate. * @param x The required x-coordinate * @return A new filter that succeeds only on shapes that have * the given x-coordinate. */ public ObjectdrawFilter xLocationIs(final int x) { return applySelfTo(ObjectdrawFilter.xLocationIs(x)); } // ---------------------------------------------------------- /** * Create a filter that checks a shape's y-coordinate. * @param y The required y-coordinate * @return A new filter that succeeds only on shapes that have * the given y-coordinate. */ public ObjectdrawFilter yLocationIs(final int y) { return applySelfTo(ObjectdrawFilter.yLocationIs(y)); } // ---------------------------------------------------------- /** * Create a filter that checks a shape's location. * @param x The required x-coordinate. * @param y The required y-coordinate. * @return A new filter that succeeds only on shapes that have * the given location. */ public ObjectdrawFilter locationIs(final int x, final int y) { return applySelfTo(ObjectdrawFilter.locationIs(x, y)); } // ---------------------------------------------------------- /** * Create a filter that checks whether a shape's location (its * top left corner) lies within a specific rectangle. * @param region A rectangle defining a region. * @return A new filter that succeeds only on components that have * a location within the given region. */ public ObjectdrawFilter isLocatedWithin(final Rectangle region) { return applySelfTo(ObjectdrawFilter.isLocatedWithin(region)); } // ---------------------------------------------------------- /** * Create a filter that checks whether a shape's bounding box * lies within a specific rectangle--that is, whether the entire * shape's area rather than just its top left corner, lies within * the specified region. * @param region A rectangle defining a region. * @return A new filter that succeeds only on shapes that * lie entirely within the given region, as determined by * {@link Rectangle#contains(Rectangle)}. */ public ObjectdrawFilter isContainedWithin(final Rectangle region) { return applySelfTo(ObjectdrawFilter.isContainedWithin(region)); } // ---------------------------------------------------------- /** * Create a filter that checks a shape's color. * @param color the color to check for. * @return A new filter that succeeds only on shapes that have the * specified color. */ public ObjectdrawFilter colorIs(final Color color) { return applySelfTo(ObjectdrawFilter.colorIs(color)); } // ---------------------------------------------------------- /** * Create a filter that checks whether a shape is hidden. * @param hidden a boolean indicating the hidden state to check for. * @return A new filter that succeeds only if the shape's hidden * value matches hidden. */ public ObjectdrawFilter hiddenIs(final boolean hidden) { return applySelfTo(ObjectdrawFilter.hiddenIs(hidden)); } // ---------------------------------------------------------- /** * Create a filter that checks whether a line starts at a given * coordinate. * @param x the required x-coordinate. * @param y the required y-coordinate. * @return A new filter that succeeds only for lines that start at * (x, y). */ public ObjectdrawFilter lineStartLocationIs(final int x, final int y) { return applySelfTo( ObjectdrawFilter.lineStartPointIs(new Location(x, y))); } // ---------------------------------------------------------- /** * Create a filter that checks whether a line starts at a given * location. * @param location the required Location. * @return A new filter that succeeds only for lines that start at * the specified location. */ public ObjectdrawFilter lineStartLocationIs(final Location location) { return applySelfTo(ObjectdrawFilter.lineStartPointIs(location)); } // ---------------------------------------------------------- /** * Create a filter that checks whether a line ends at a given * coordinate. * @param x the required x-coordinate. * @param y the required y-coordinate. * @return A new filter that succeeds only for lines that end * at (x, y). */ public ObjectdrawFilter lineEndLocationIs(final int x, final int y) { return applySelfTo( ObjectdrawFilter.lineEndPointIs(new Location(x, y))); } // ---------------------------------------------------------- /** * Create a filter that checks whether a line starts at a given * location. * @param location the required Location * @return A new filter that succeeds only lines that end at location. */ public ObjectdrawFilter lineEndLocationIs(final Location location) { return applySelfTo(ObjectdrawFilter.lineEndPointIs(location)); } // ---------------------------------------------------------- /** * 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 ObjectdrawFilter applySelfTo( final ObjectdrawFilter 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 ObjectdrawFilter not(final ObjectdrawFilter 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 ObjectdrawFilter applySelfTo(final ObjectdrawFilter otherFilter) { return new ObjectdrawFilter(description( ObjectdrawFilter.this.toString(), otherFilter.toString())) { public boolean test(DrawableInterface shape) { return combine(ObjectdrawFilter.this.test(shape), otherFilter.test(shape)); } }; } // ---------------------------------------------------------- /** * 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(ObjectdrawFilter)}. * * @param otherFilter The second argument to "and". * @return A new filter object that represents "this AND otherFilter". */ public final ObjectdrawFilter and(final ObjectdrawFilter otherFilter) { final ObjectdrawFilter self = this; ObjectdrawFilter odf = new ObjectdrawFilter( "(" + this + " AND " + otherFilter + ")") { public boolean test(DrawableInterface shape) { return self.test(shape) && otherFilter.test(shape); } }; return odf; } // ---------------------------------------------------------- /** * 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.heightIs(10).or(hiddenIs(true).and.widthIs(20))</code>. * If you wish to use the <code>.or.</code> notation instead, leaving * off the parentheses, see {@link BinaryOperator#or(ObjectdrawFilter)}. * * @param otherFilter The second argument to "or". * @return A new filter object that represents "this OR otherFilter". */ public final ObjectdrawFilter or(final ObjectdrawFilter otherFilter) { final ObjectdrawFilter self = this; ObjectdrawFilter odf = new ObjectdrawFilter( "(" + this + " OR " + otherFilter + ")") { public boolean test(DrawableInterface shape) { return self.test(shape) || otherFilter.test(shape); } }; return odf; } // ---------------------------------------------------------- /** * Evaluate whether a DrawableInterface object 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 shape The DrawableInterface object to check. * @return true if the component matches this filter. */ public abstract boolean test(DrawableInterface shape); // ---------------------------------------------------------- /** * 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 ObjectdrawFilter.WhereOperator where = * new ObjectdrawFilter.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 ObjectdrawFilter applySelfTo(ObjectdrawFilter 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 ObjectdrawFilter not(final ObjectdrawFilter otherFilter) { return primitiveNot(otherFilter); } } //~ Private Methods/Declarations .......................................... // ---------------------------------------------------------- private static ObjectdrawFilter textIs(final String text) { ObjectdrawFilter odf = new ObjectdrawFilter("text = \"" + text + "\"") { public boolean test(DrawableInterface shape) { Method m = null; try { m = shape.getClass().getMethod("getText"); return ((String)m.invoke(shape)).equals(text); } catch ( Exception e ) { return false; } } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter widthIs(final int value) { ObjectdrawFilter odf = new ObjectdrawFilter("width = " + value) { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable2DInterface) && ((Drawable2DInterface)shape).getWidth() == value; } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter heightIs(final int value) { ObjectdrawFilter odf = new ObjectdrawFilter("height = " + value) { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable2DInterface) && ((Drawable2DInterface)shape).getHeight() == value; } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter sizeIs( final int width, final int height) { ObjectdrawFilter odf = new ObjectdrawFilter( "size = (" + width + ", " + height + ")") { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable2DInterface) && ((Drawable2DInterface)shape).getWidth() == width && ((Drawable2DInterface)shape).getHeight() == height; } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter sizeIsWithin( final int maxWidth, final int maxHeight) { ObjectdrawFilter odf = new ObjectdrawFilter( "sizeIsWithin(" + maxWidth + ", " + maxHeight + ")") { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable2DInterface) && ((Drawable2DInterface)shape).getWidth() <= maxWidth && ((Drawable2DInterface)shape).getHeight() <= maxHeight; } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter xLocationIs(final int value) { ObjectdrawFilter odf = new ObjectdrawFilter("xLocation = " + value) { public boolean test(DrawableInterface shape) { return(shape instanceof Drawable2DInterface) && ((Drawable2DInterface)shape).getX() == value; } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter yLocationIs(final int value) { ObjectdrawFilter odf = new ObjectdrawFilter("yLocation = " + value) { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable2DInterface) && ((Drawable2DInterface)shape).getY() == value; } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter locationIs(final int x, final int y) { ObjectdrawFilter odf = new ObjectdrawFilter( "location = (" + x + ", " + y + ")") { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable2DInterface) && ((Drawable2DInterface)shape).getX() == x && ((Drawable2DInterface)shape).getY() == y; } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter isLocatedWithin( final Rectangle region) { ObjectdrawFilter odf = new ObjectdrawFilter( "isLocatedWithin(" + region + ")") { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable2DInterface) && region.contains( ((Drawable2DInterface)shape).getLocation().toPoint()); } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter isContainedWithin( final Rectangle region) { ObjectdrawFilter odf = new ObjectdrawFilter( "isContainedWithin(" + region + ")") { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable2DInterface) && region.contains(new Rectangle( (int)((Drawable2DInterface)shape).getX(), (int)((Drawable2DInterface)shape).getY(), (int)((Drawable2DInterface)shape).getWidth(), (int)((Drawable2DInterface)shape).getHeight())); } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter hiddenIs(final boolean hidden) { ObjectdrawFilter odf = new ObjectdrawFilter( "hidden = " + hidden) { public boolean test(DrawableInterface shape) { return shape.isHidden() == hidden; } }; return odf; } // ---------------------------------------------------------- private static ObjectdrawFilter typeIs( final Class<? extends DrawableInterface> aClass) { ObjectdrawFilter odf = new ObjectdrawFilter( "type = " + aClass.getSimpleName()) { public boolean test(DrawableInterface shape) { return aClass.isAssignableFrom(shape.getClass()); } }; return odf; } // ---------------------------------------------------------- private static ObjectdrawFilter colorIs(final Color color) { ObjectdrawFilter odf = new ObjectdrawFilter("color = " + color) { public boolean test(DrawableInterface shape) { return shape.getColor() == color; } }; return odf; } // ---------------------------------------------------------- private static ObjectdrawFilter lineStartPointIs(final Location location) { ObjectdrawFilter odf = new ObjectdrawFilter("line starts at (" + location.getX() + ", " + location.getY() +")") { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable1DInterface) && ((Drawable1DInterface)shape).getStart().equals(location); } }; return odf; } // ---------------------------------------------------------- private static ObjectdrawFilter lineEndPointIs(final Location location) { ObjectdrawFilter odf = new ObjectdrawFilter("line ends at (" + location.getX() + ", " + location.getY() +")") { public boolean test(DrawableInterface shape) { return (shape instanceof Drawable1DInterface) && ((Drawable1DInterface)shape).getEnd().equals(location); } }; return odf; } // ---------------------------------------------------------- private static final ObjectdrawFilter primitiveNot( final ObjectdrawFilter otherFilter) { return new ObjectdrawFilter("(NOT " + otherFilter + ")") { public boolean test(DrawableInterface shape) { return !otherFilter.test(shape); } }; } }