GT-3515 - Updated documentation for column constraints special text

filtering section; updated runtime type discovery to examine interfaces
This commit is contained in:
dragonmacher 2020-01-30 15:24:11 -05:00
parent 283e148b26
commit 491c4e466a
6 changed files with 255 additions and 39 deletions

View file

@ -22,11 +22,11 @@ import ghidra.util.SystemUtilities;
/**
* Class that maps one type of column constraint into another. Typically, these are created
* automatically based on {@link ColumnTypeMapper} that are discovered by the system. For example,
*{@literal if you have a column type of "Foo", and you create a ColumnTypeMapper<Foo, String>, then all the}
* string constraints would now be available that column.
* {@literal if you have a column type of "Foo", and you create a ColumnTypeMapper<Foo, String>,
* then all the} string constraints would now be available that column.
*
* @param <T> The column type.
* @param <M> the converted (mapped) type.
* @param <T> The column type
* @param <M> the converted (mapped) type
*/
public class MappedColumnConstraint<T, M> implements ColumnConstraint<T> {

View file

@ -27,10 +27,14 @@ import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.table.column.GColumnRenderer.ColumnConstraintFilterMode;
/**
* Class for maintaining information about a table's column for the purpose of configuring filters
* based on that columns values. These are generated by examining a table's column types and finding
* out if there are any ColumnConstraints that support that type. If so, a ColumnFilterData is
* created for that column which then allows filtering on that columns data.
* This class provides all known {@link ColumnConstraint}s for a given table column.
*
* <P>Class for maintaining information about a particular table's column for the purpose of
* configuring filters based on that column's values. Instances of this class are generated
* by examining a table's column types and finding any {@link ColumnConstraint}s that support
* that type. If column constraints are found, a {@link ColumnFilterData} is created for that column
* which then allows filtering on that columns data via the column constraints mechanism (which
* is different than the traditional text filter).
*
* @param <T> the column type.
*/
@ -61,12 +65,20 @@ public class ColumnFilterData<T> implements Comparable<ColumnFilterData<T>> {
private List<ColumnConstraint<T>> initializeConstraints(RowObjectFilterModel<?> model,
Class<T> columnClass) {
//
// Case 1: the column is not dynamic and thus has no way of overriding the column
// constraint filtering mechanism.
//
Collection<ColumnConstraint<T>> defaultConstraints =
DiscoverableTableUtils.getColumnConstraints(columnClass);
if (!(model instanceof DynamicColumnTableModel)) {
return new ArrayList<>(defaultConstraints);
}
//
// Case 2: the column is dynamic, but does not supply a specialized column renderer,
// which is the means for overriding the column constraint filtering mechanism.
//
DynamicColumnTableModel<?> columnBasedModel = (DynamicColumnTableModel<?>) model;
DynamicTableColumn<?, ?, ?> column = columnBasedModel.getColumn(modelIndex);
GColumnRenderer<?> columnRenderer = column.getColumnRenderer();
@ -74,19 +86,31 @@ public class ColumnFilterData<T> implements Comparable<ColumnFilterData<T>> {
return new ArrayList<>(defaultConstraints);
}
//
// Case 3: the column renderer has signaled that it uses only column constraint filtering
// and does not support the traditional text based filtering.
//
ColumnConstraintFilterMode mode = columnRenderer.getColumnConstraintFilterMode();
if (mode == ColumnConstraintFilterMode.USE_COLUMN_CONSTRAINTS_ONLY) {
return new ArrayList<>(defaultConstraints);
}
@SuppressWarnings("unchecked")
//
// Case 4: the column supports text filtering. Find any column constraints for the
// column's type. Then, create string-based constraints that will filter on
// the column's conversion from its type to a string (via
// GColumnRenderer.getFilterString()).
//
@SuppressWarnings("unchecked") // See type note on the class below
GColumnRenderer<T> asT = (GColumnRenderer<T>) columnRenderer;
ColumnRendererMapper mapper = new ColumnRendererMapper(asT, columnBasedModel, modelIndex);
Collection<ColumnConstraint<T>> rendererStringConstraints =
DiscoverableTableUtils.getColumnConstraints(mapper);
//
// Case 5: the renderer supports both text filtering and column constraint filtering.
//
List<ColumnConstraint<T>> results = new ArrayList<>(rendererStringConstraints);
if (mode == ColumnConstraintFilterMode.USE_BOTH_COLUMN_RENDERER_FITLER_STRING_AND_CONSTRAINTS) {
// also use the normal constraints with the renderer constraints
results.addAll(defaultConstraints);
@ -213,6 +237,13 @@ public class ColumnFilterData<T> implements Comparable<ColumnFilterData<T>> {
* This class allows us to turn client columns of type <code>T</code> to a String. We use
* the renderer provided at construction time to generate a filter string when
* {@link #convert(Object)} is called.
*
* <P>Implementation Note: the type 'T' here is used to satisfy the external client's
* expected list of constraints. We will not be able to identify 'T' at runtime. Rather,
* our parent's {@link #getSourceType()} will simply be {@link Object}. This is fine, as
* this particular class will not have {@link #getSourceType()} called, due to how we
* are using it. (Normally, the source type is used to find compatible constraints; we
* are not using the discovery mechanism with this private class.)
*/
private class ColumnRendererMapper extends ColumnTypeMapper<T, String> {
@ -229,14 +260,13 @@ public class ColumnFilterData<T> implements Comparable<ColumnFilterData<T>> {
@Override
public String convert(T value) {
Settings settings = model.getColumnSettings(columnModelIndex);
if (value == null) {
return null;
}
Settings settings = model.getColumnSettings(columnModelIndex);
String s = renderer.getFilterString(value, settings);
return s;
}
}
}

View file

@ -102,9 +102,9 @@ public class ColumnFilterDialogModel<R> {
}
/**
* Creates a new filter fow (a new major row in the dialog filter panel)
* @param logicOperation the logical operation for how this row interacts with the rows before it
* @return the new filter row that represents a major row in the dialog filter panel.
* Creates a new filter for (a new major row in the dialog filter panel)
* @param logicOperation the logical operation for how this row interacts with preceding rows
* @return the new filter row that represents a major row in the dialog filter panel
*/
public DialogFilterRow createFilterRow(LogicOperation logicOperation) {

View file

@ -36,12 +36,12 @@ import ghidra.util.exception.AssertException;
* columns. The specifics of how the text filter works are defined by the
* {@link RowFilterTransformer}, which is controlled by the user via the button at the right
* of the filter field. (In the absence of this button, filters are typically a 'contains'
* filter.
* filter.)
*
* <P>The default transformer turns items to strings by, in order,:
* <OL>
* <LI>checking the the renderer's {@link #getFilterString(Object, Settings)},
* if a renderer is installed
* <LI>checking the the <b>column</b> renderer's
* {@link #getFilterString(Object, Settings)},if a column renderer is installed
* </LI>
* <LI>checking to see if the column value is an instance of {@link DisplayStringProvider}
* </LI>
@ -68,6 +68,10 @@ import ghidra.util.exception.AssertException;
* </LI>
* </OL>
*
* <P><B>Note: The default filtering behavior of this class is to only filter on the aforementioned
* filter text field. That is, column constraints will not be enabled by default. To
* change this, change the value returned by {@link #getColumnConstraintFilterMode()}.</B>
*
* @param <T> the column type
*/
public interface GColumnRenderer<T> extends TableCellRenderer {

View file

@ -18,6 +18,7 @@ package utilities.util.reflection;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
@ -153,6 +154,84 @@ public class ReflectionUtilitiesTest {
babyTypeArguments.get(1));
}
@Test(expected = NullPointerException.class)
public void testRuntimeTypeDiscovery_Null() {
ReflectionUtilities.getTypeArguments(List.class, null);
}
@Test
public void testRumtimeTypeDiscovery_AnonymousClass() {
List<String> myList = new ArrayList<String>() {
// stub
};
List<Class<?>> types = ReflectionUtilities.getTypeArguments(List.class, myList.getClass());
assertEquals(1, types.size());
assertEquals(String.class, types.get(0));
}
@Test
public void testRumtimeTypeDiscovery_LocalVariable() {
List<String> myList = new ArrayList<String>();
List<Class<?>> types = ReflectionUtilities.getTypeArguments(List.class, myList.getClass());
assertEquals(1, types.size());
assertNull(types.get(0));
}
@Test
public void testRuntimeTypeDiscovery_MixedHierarchy_AbstractClassAndInterfaceBothDefineValues() {
//
// Test to make sure that we get not only a directly hierarchy, but the lateral one
// as well, where we pursue interfaces that may have defined some types.
//
List<Class<?>> types = ReflectionUtilities.getTypeArguments(RuntimeBaseInterface.class,
ChildExtendingPartiallyDefinedTypes.class);
assertEquals(2, types.size());
assertEquals(String.class, types.get(0));
assertEquals(Double.class, types.get(1));
}
@Test
public void testRuntimeTypeDiscovery_SubInterfaceDefinesValues() {
//
// Test to make sure that we get not only a directly hierarchy, but the lateral one
// as well, where we pursue interfaces that may have defined some types.
//
List<Class<?>> types = ReflectionUtilities.getTypeArguments(RuntimeBaseInterface.class,
ChildExtendingWhollyDefinedTypes.class);
assertEquals(2, types.size());
assertEquals(String.class, types.get(0));
assertEquals(Double.class, types.get(1));
}
@Test
public void testRuntimeTypeDiscovery_MixedHierarchy_UnrelatedParents() {
//
// Test to make sure that we get not only a directly hierarchy, but the lateral one
// as well, where we pursue interfaces that may have defined some types.
//
// This test also verifies that in a mixed type hierarchy, we can correctly locate types
// depending upon the parent type we pass in.
//
List<Class<?>> types = ReflectionUtilities.getTypeArguments(RuntimeBaseInterface.class,
ChildWithMixedParentTypes.class);
assertEquals(2, types.size());
assertEquals(String.class, types.get(0));
assertEquals(Double.class, types.get(1));
types = ReflectionUtilities.getTypeArguments(List.class,
ChildWithMixedParentTypes.class);
assertEquals(1, types.size());
assertEquals(Integer.class, types.get(0));
}
//==================================================================================================
// Inner Classes
//==================================================================================================
@ -179,6 +258,39 @@ public class ReflectionUtilitiesTest {
}
}
private interface RuntimeBaseInterface<T, J> {
// stub
}
private interface PartiallyDefinedInterface<J> extends RuntimeBaseInterface<String, J> {
// stub
}
private interface WhollyDefinedInterface extends RuntimeBaseInterface<String, Double> {
// stub
}
private class AbstractPartiallyDefinedClass<I> implements RuntimeBaseInterface<I, Double> {
// stub
}
private class ChildExtendingPartiallyDefinedTypes
extends AbstractPartiallyDefinedClass<String>
implements PartiallyDefinedInterface<Double> {
// stub
}
private class ChildExtendingWhollyDefinedTypes
implements WhollyDefinedInterface {
// stub
}
private class ChildWithMixedParentTypes
extends ArrayList<Integer>
implements WhollyDefinedInterface {
// stub
}
private class RuntimeBaseType<T, J> {
// stub
}

View file

@ -560,15 +560,46 @@ public class ReflectionUtilities {
}
}
/**
* Returns the type arguments for the given base class and extension.
*
* <p>Caveat: this lookup will only work if the given child class is a concrete class that
* has its type arguments specified. For example, these cases will work:
* <pre>
* // anonymous class definition
* List&lt;String&gt; myList = new ArrayList&lt;String&gt;() {
* ...
* };
*
* // class definition
* public class MyList implements List&lt;String&gt; {
* </pre>
*
* Whereas this case will not work:
* <pre>
* // local variable with the type specified
* List&lt;String&gt; myList = new ArrayList&lt;String&gt;();
* </pre>
*
* <p>Note: a null entry in the result list will exist for any type that was unrecoverable
*
*
* @param <T> the type of the base and child class
* @param baseClass the base class
* @param childClass the child class
* @return the type arguments
*/
public static <T> List<Class<?>> getTypeArguments(Class<T> baseClass,
Class<? extends T> childClass) {
Map<Type, Type> resolvedTypesDictionary = new HashMap<>();
Objects.requireNonNull(baseClass);
Objects.requireNonNull(childClass);
Map<Type, Type> resolvedTypesDictionary = new HashMap<>();
Type baseClassAsType =
walkClassHierarchyAndResolveTypes(baseClass, resolvedTypesDictionary, childClass);
// now see if we can resolve the type arguments defined by 'baseClass' to the raw runtime
// class that is in use
// try to resolve type arguments defined by 'baseClass' to the raw runtime class
Type[] baseClassDeclaredTypeArguments = getDeclaredTypeArguments(baseClassAsType);
return resolveBaseClassTypeArguments(resolvedTypesDictionary,
baseClassDeclaredTypeArguments);
@ -577,29 +608,69 @@ public class ReflectionUtilities {
private static <T> Type walkClassHierarchyAndResolveTypes(Class<T> baseClass,
Map<Type, Type> resolvedTypes, Type type) {
while (!getClass(type).equals(baseClass)) {
if (type instanceof Class) {
type = ((Class<?>) type).getGenericSuperclass();
}
else {
ParameterizedType parameterizedType = (ParameterizedType) type;
Class<?> rawType = (Class<?>) parameterizedType.getRawType();
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
for (int i = 0; i < actualTypeArguments.length; i++) {
resolvedTypes.put(typeParameters[i], actualTypeArguments[i]);
}
if (type == null) {
return null;
}
if (!rawType.equals(baseClass)) {
type = rawType.getGenericSuperclass();
if (equals(type, baseClass)) {
return type;
}
if (type instanceof Class) {
Class<?> clazz = (Class<?>) type;
Type[] interfaceTypes = clazz.getGenericInterfaces();
Set<Type> toCheck = new HashSet<>();
toCheck.addAll(Arrays.asList(interfaceTypes));
Type parentType = clazz.getGenericSuperclass();
toCheck.add(parentType);
for (Type t : toCheck) {
Type result = walkClassHierarchyAndResolveTypes(baseClass, resolvedTypes, t);
if (equals(result, baseClass)) {
return result;
}
}
if (type == null) {
return type;
return parentType;
}
ParameterizedType parameterizedType = (ParameterizedType) type;
Class<?> rawType = (Class<?>) parameterizedType.getRawType();
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
for (int i = 0; i < actualTypeArguments.length; i++) {
resolvedTypes.put(typeParameters[i], actualTypeArguments[i]);
}
if (rawType.equals(baseClass)) {
return rawType;
}
Type[] interfaceTypes = rawType.getGenericInterfaces();
Set<Type> toCheck = new HashSet<>();
toCheck.addAll(Arrays.asList(interfaceTypes));
Type parentType = rawType.getGenericSuperclass();
toCheck.add(parentType);
for (Type t : toCheck) {
Type result = walkClassHierarchyAndResolveTypes(baseClass, resolvedTypes, t);
if (equals(result, baseClass)) {
return result;
}
}
return type;
return parentType;
}
private static boolean equals(Type type, Class<?> c) {
Class<?> typeClass = getClass(type);
if (typeClass == null) {
return false;
}
return typeClass.equals(c);
}
private static Class<?> getClass(Type type) {
@ -637,7 +708,6 @@ public class ReflectionUtilities {
return typeArgumentsAsClasses;
}
// we checked
private static Type[] getDeclaredTypeArguments(Type type) {
if (type instanceof Class) {
return ((Class<?>) type).getTypeParameters();