mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
GT-3515 - Updated documentation for column constraints special text
filtering section; updated runtime type discovery to examine interfaces
This commit is contained in:
parent
283e148b26
commit
491c4e466a
6 changed files with 255 additions and 39 deletions
|
@ -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> {
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<String> myList = new ArrayList<String>() {
|
||||
* ...
|
||||
* };
|
||||
*
|
||||
* // class definition
|
||||
* public class MyList implements List<String> {
|
||||
* </pre>
|
||||
*
|
||||
* Whereas this case will not work:
|
||||
* <pre>
|
||||
* // local variable with the type specified
|
||||
* List<String> myList = new ArrayList<String>();
|
||||
* </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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue