/*==========================================================================*\
| $Id: Filter.java,v 1.3 2011/06/09 15:31:24 stedwar2 Exp $
|*-------------------------------------------------------------------------*|
| Copyright (C) 2011 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.reflection;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
//-------------------------------------------------------------------------
/**
* TODO: document.
*
* @param <ConcreteFilterType> A parameter indicating the concrete subclass
* of this class, for use in providing more specialized return types on
* some methods.
* @param <FilteredObjectType> A parameter indicating the kind of object
* this filter accepts.
*
* @author Stephen Edwards
* @author Last changed by $Author: stedwar2 $
* @version $Revision: 1.3 $, $Date: 2011/06/09 15:31:24 $
*/
public abstract class Filter<ConcreteFilterType, FilteredObjectType>
implements Iterable<ConcreteFilterType>
{
//~ Fields ................................................................
private Filter<ConcreteFilterType, FilteredObjectType> previousFilter;
private List<FilteredObjectType> filteredCandidates;
private String descriptionOfConstraint;
private int hashCode = 0;
//~ Constructor ...........................................................
// ----------------------------------------------------------
/**
* Create a new VisibilityFilter object.
* @param previous The previous filter in the chain of filters.
* @param descriptionOfConstraint A description of the constraint imposed
* by this filter (just one step in the chain).
*/
@SuppressWarnings("unchecked")
protected Filter(Filter<?, ?> previous, String descriptionOfConstraint)
{
previousFilter =
(Filter<ConcreteFilterType, FilteredObjectType>)previous;
this.descriptionOfConstraint = descriptionOfConstraint;
}
//~ Public Methods ........................................................
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
public int count()
{
filter();
return filteredCandidates.size();
}
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
public boolean exists()
{
return guaranteesMultipleMatches() || count() > 0;
}
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
public boolean isUnique()
{
return !guaranteesMultipleMatches() && count() == 1;
}
// ----------------------------------------------------------
/**
* Get the unencapsulated (raw) object represented by this filter.
* This operation requires there to exist a unique match for the
* filter or a {@link ReflectionError} will be thrown. Use
* {@link #allMatches()} if there is not a unique match.
* @return The raw (unencapsulated} object represented by this filter.
*/
public FilteredObjectType raw()
{
return uniqueMatch();
}
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
public List<FilteredObjectType> allMatches()
{
filter();
return filteredCandidates;
}
// ----------------------------------------------------------
/**
* Get an iterator over all instances this filter matches, where the
* items traversed by the iterator are ConcreteFilterType instances.
* @return The iterator
*/
public Iterator<ConcreteFilterType> iterator()
{
return new Iterator<ConcreteFilterType>()
{
private List<FilteredObjectType> matches = allMatches();
private int pos = 0;
// ----------------------------------------------------------
public boolean hasNext()
{
return pos < matches.size();
}
// ----------------------------------------------------------
public ConcreteFilterType next()
{
return createFreshFilter(matches.get(pos++));
}
// ----------------------------------------------------------
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
// ----------------------------------------------------------
/**
* Get a human-readable description of this filter.
* @return A human-readable description of this filter.
*/
public String description()
{
return description(false);
}
// ----------------------------------------------------------
/**
* Get a human-readable description of this filter.
* @return A human-readable description of this filter.
*/
public String toString()
{
if (isUnique())
{
return raw().toString();
}
else
{
return description();
}
}
// ----------------------------------------------------------
@Override
public int hashCode()
{
if (hashCode == 0)
{
hashCode =
(new HashSet<FilteredObjectType>(allMatches())).hashCode();
}
return hashCode;
}
// ----------------------------------------------------------
/**
* Determine whether this object is equal to the another.
* @param other The object to compare against.
* @return True if this object is equal to the other.
*/
public boolean equals(final Object other)
{
if (other == this)
{
return true;
}
if (other == null)
{
return false;
}
if (other instanceof Filter)
{
@SuppressWarnings("unchecked")
Filter<ConcreteFilterType, FilteredObjectType> otherType =
(Filter<ConcreteFilterType, FilteredObjectType>)other;
if (description().equals(otherType.description()))
{
return true;
}
Set<FilteredObjectType> left =
new HashSet<FilteredObjectType>(allMatches());
Set<FilteredObjectType> right =
new HashSet<FilteredObjectType>(otherType.allMatches());
return left.equals(right);
}
else
{
return quantify.evaluate(new Predicate<FilteredObjectType>()
{
public boolean isSatisfiedBy(FilteredObjectType object)
{
return other.equals(object);
}
});
}
}
// ----------------------------------------------------------
/**
* Change the quantification behavior of a filter so that boolean
* predicates are evaluated over all matches for the filter and the
* result is "or"ed together.
* @param filter The filter to alter.
* @param <ConcreteFilterType> This type is deduced from the filter.
* @param <FilteredObjectType> This type is deduced from the filter.
* @return A new filter that behaves like the different one, but uses
* the desired quantification strategy.
*/
@SuppressWarnings("unchecked")
public static <ConcreteFilterType, FilteredObjectType>
ConcreteFilterType atLeastOne(ConcreteFilterType filter)
{
ConcreteFilterType result =
((Filter<ConcreteFilterType, FilteredObjectType>)filter)
.createFreshFilter(filter, null);
Filter<ConcreteFilterType, FilteredObjectType> f =
(Filter<ConcreteFilterType, FilteredObjectType>)result;
f.quantify = f.AT_LEAST_ONE;
return result;
}
// ----------------------------------------------------------
/**
* Change the quantification behavior of a filter so that boolean
* predicates are evaluated over all matches for the filter and the
* result is "xor"ed together.
* @param filter The filter to alter.
* @param <ConcreteFilterType> This type is deduced from the filter.
* @param <FilteredObjectType> This type is deduced from the filter.
* @return A new filter that behaves like the different one, but uses
* the desired quantification strategy.
*/
@SuppressWarnings("unchecked")
public static <ConcreteFilterType, FilteredObjectType>
ConcreteFilterType onlyOne(ConcreteFilterType filter)
{
ConcreteFilterType result =
((Filter<ConcreteFilterType, FilteredObjectType>)filter)
.createFreshFilter(filter, null);
Filter<ConcreteFilterType, FilteredObjectType> f =
(Filter<ConcreteFilterType, FilteredObjectType>)result;
f.quantify = f.ONLY_ONE;
return result;
}
// ----------------------------------------------------------
/**
* Change the quantification behavior of a filter so that boolean
* predicates are evaluated over all matches for the filter and the
* result is "and"ed together.
* @param filter The filter to alter.
* @param <ConcreteFilterType> This type is deduced from the filter.
* @param <FilteredObjectType> This type is deduced from the filter.
* @return A new filter that behaves like the different one, but uses
* the desired quantification strategy.
*/
@SuppressWarnings("unchecked")
public static <ConcreteFilterType, FilteredObjectType>
ConcreteFilterType every(ConcreteFilterType filter)
{
ConcreteFilterType result =
((Filter<ConcreteFilterType, FilteredObjectType>)filter)
.createFreshFilter(filter, null);
Filter<ConcreteFilterType, FilteredObjectType> f =
(Filter<ConcreteFilterType, FilteredObjectType>)result;
f.quantify = f.EVERY;
return result;
}
//~ Protected Methods .....................................................
// ----------------------------------------------------------
/**
* Create a new instance of the same class as "this", initialized
* with these values.
* @param previous The previous filter in the chain of filters.
* @param descriptionOfThisStage A description of this stage in the
* filter chain.
* @return The newly created Filter instance.
*/
protected abstract ConcreteFilterType
createFreshFilter(
ConcreteFilterType previous, String descriptionOfThisStage);
// ----------------------------------------------------------
/**
* Create a new instance of the same class as "this", representing a
* single object.
* @param object The single value of the new filter.
* @return The newly created Filter instance.
*/
protected abstract ConcreteFilterType
createFreshFilter(final FilteredObjectType object);
// ----------------------------------------------------------
/**
* Get a human-readable name for the type of objects to which this filter
* applies. The result should be in the singular form.
* @return A human-readable version of the FilteredObjectType.
*/
protected abstract String filteredObjectDescription();
// ----------------------------------------------------------
/**
* Get the plural form of {@link #filteredObjectDescription()}.
* @return A human-readable version of the plural form of
* FilteredObjectType.
*/
protected String filteredObjectsDescription()
{
return filteredObjectDescription() + "s";
}
// ----------------------------------------------------------
/**
* Get a human-readable description of this filter.
* @param result A StringBuilder to add the description to.
*/
protected void addDescriptionOfConstraint(StringBuilder result)
{
if (previousFilter != null)
{
previousFilter.addDescriptionOfConstraint(result);
}
if (descriptionOfConstraint != null)
{
if (result.length() > 0)
{
result.append(' ');
}
result.append(descriptionOfConstraint);
}
}
// ----------------------------------------------------------
/**
* Get a human-readable description of this filter.
* @param plural Whether to generate the singular (false) or
* plural (true) form of the description.
* @return A human-readable description of this filter.
*/
protected String description(boolean plural)
{
StringBuilder result = new StringBuilder();
result.append(plural
? filteredObjectsDescription()
: filteredObjectDescription());
addDescriptionOfConstraint(result);
return result.toString();
}
// ----------------------------------------------------------
/**
* Get a description of the specified object, suitable for use in
* a diagnostic message. The default implementation just uses
* <code>toString()</code> on the object.
* @param object The object to describe.
* @return a description of the object.
*/
protected String describe(FilteredObjectType object)
{
return "" + object;
}
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
protected boolean guaranteesMultipleMatches()
{
if (previousFilter() != null)
{
return previousFilter().guaranteesMultipleMatches();
}
else
{
return false;
}
}
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
protected FilteredObjectType firstMatch()
{
filter();
if (!exists())
{
throw new ReflectionError(
"No " + description(false) + " was found.");
}
return filteredCandidates.get(0);
}
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
protected FilteredObjectType uniqueMatch()
{
firstMatch();
if (filteredCandidates.size() > 1)
{
StringBuilder msg = new StringBuilder();
msg.append(filteredCandidates.size());
msg.append(' ');
msg.append(description(true));
msg.append(" were found: [");
FilteredObjectType first = filteredCandidates.get(0);
for (FilteredObjectType object : filteredCandidates)
{
if (object != first)
{
msg.append(", ");
}
msg.append(describe(object));
}
msg.append("].");
throw new ReflectionError(msg.toString());
}
return filteredCandidates.get(0);
}
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
protected Filter<ConcreteFilterType, FilteredObjectType> previousFilter()
{
return previousFilter;
}
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
protected List<FilteredObjectType> candidatesFromThisFilter()
{
return null;
}
// ----------------------------------------------------------
/**
* TODO: document.
* @return TODO: describe
*/
protected final List<FilteredObjectType> allCandidates()
{
List<FilteredObjectType> result = candidatesFromThisFilter();
if (result == null)
{
if (previousFilter != null)
{
result = previousFilter.allCandidates();
}
else
{
// TODO: need a good ReflectionError message
assert false;
}
}
return result;
}
// ----------------------------------------------------------
/**
* TODO: document.
* @param object TODO: describe
* @return TODO: describe
*/
protected abstract boolean thisFilterAccepts(FilteredObjectType object);
// ----------------------------------------------------------
/**
* TODO: document.
* @param object TODO: describe
* @return TODO: describe
*/
protected final boolean accept(FilteredObjectType object)
{
boolean result = true;
if (previousFilter != null)
{
result = previousFilter.accept(object);
}
return result && thisFilterAccepts(object);
}
// ----------------------------------------------------------
/**
* TODO: document.
*/
protected void flush()
{
filteredCandidates = null;
}
// ----------------------------------------------------------
/**
* TODO: document.
*/
protected final void filter()
{
if (filteredCandidates == null)
{
filteredCandidates = filter(allCandidates());
}
}
// ----------------------------------------------------------
/**
* TODO: document.
* @param candidates TODO: describe
* @return TODO: describe
*/
protected List<FilteredObjectType> filter(
List<FilteredObjectType> candidates)
{
List<FilteredObjectType> result = new ArrayList<FilteredObjectType>();
if (candidates != null)
{
for (FilteredObjectType object : candidates)
{
if (accept(object))
{
result.add(object);
}
}
}
return result;
}
// ----------------------------------------------------------
/**
* This interface represents a "strategy" for evaluating a single
* object (eventually of the FilteredObjectType) for some property
* and returning true or false. It serves as the argument type for
* {@link Filter.QuantifierBehavior#evaluate(Predicate)}.
* @param <ObjectType> The type of object to which this predicate can
* be applied.
*/
protected static interface Predicate<ObjectType>
{
// ----------------------------------------------------------
/**
* Perform a test on an object, returning true or false.
* @param object The object to test.
* @return True if the object satisfies this predicate.
*/
public boolean isSatisfiedBy(ObjectType object);
}
// ----------------------------------------------------------
/**
* I wanted to use an enum for this, but the enum keyword creates
* constants that are static, while I need non-static instances
* so they can access object state.
*
* The instances of this type represent the quantifier behaviors
* supported by this class. Each constant represents a parameterized
* strategy for evaluating a predicate.
*/
protected abstract class QuantifierBehavior
{
// ----------------------------------------------------------
/**
* Evaluate a boolean predicate over a set of the matches from this
* filter, as determined by the filter's current mode.
* @param predicate The predicate to evaluate
* @return The result of applying the predicate to this filter's
* matches, as determined by this filter's quantifier behavior.
*/
public abstract boolean evaluate(
Predicate<FilteredObjectType> predicate);
}
//~ Private Methods .......................................................
// ----------------------------------------------------------
/**
* This constant represents an evaluation strategy that
* "or"s together the results of the predicate across all matches.
*/
private final QuantifierBehavior AT_LEAST_ONE = new QuantifierBehavior()
{
public boolean evaluate(
Predicate<FilteredObjectType> predicate)
{
for (FilteredObjectType object : allMatches())
{
if (predicate.isSatisfiedBy(object))
{
return true;
}
}
return false;
}
};
// ----------------------------------------------------------
/**
* This constant represents an evaluation strategy that
* "xor"s together the results of the predicate across all matches.
*/
private final QuantifierBehavior ONLY_ONE = new QuantifierBehavior()
{
public boolean evaluate(
Predicate<FilteredObjectType> predicate)
{
boolean previouslySatisfied = false;
for (FilteredObjectType object : allMatches())
{
if (predicate.isSatisfiedBy(object))
{
if (previouslySatisfied)
{
return false;
}
else
{
previouslySatisfied = true;
}
}
}
return previouslySatisfied;
}
};
// ----------------------------------------------------------
/**
* This constant represents an evaluation strategy that
* "and"s together the results of the predicate across all matches.
*/
private final QuantifierBehavior EVERY = new QuantifierBehavior()
{
public boolean evaluate(
Predicate<FilteredObjectType> predicate)
{
for (FilteredObjectType object : allMatches())
{
if (!predicate.isSatisfiedBy(object))
{
return false;
}
}
return true;
}
};
/**
* Determines the quantification behavior of this filter for boolean
* predicates (i.e, must all objects massing the filter satisfy the
* condition, or at least one, etc.).
*
* This declaration has to be down here, instead of up at the top,
* because EVERY has to be declared first before it can be used here.
*/
protected QuantifierBehavior quantify = EVERY;
}