diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/MappedColumnConstraint.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/MappedColumnConstraint.java index 5a55333eb7..5467ea784a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/MappedColumnConstraint.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/MappedColumnConstraint.java @@ -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, 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, + * then all the} string constraints would now be available that column. * - * @param The column type. - * @param the converted (mapped) type. + * @param The column type + * @param the converted (mapped) type */ public class MappedColumnConstraint implements ColumnConstraint { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterData.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterData.java index eb3d62cdab..035af40adf 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterData.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterData.java @@ -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. + * + *

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 the column type. */ @@ -61,12 +65,20 @@ public class ColumnFilterData implements Comparable> { private List> initializeConstraints(RowObjectFilterModel model, Class columnClass) { + // + // Case 1: the column is not dynamic and thus has no way of overriding the column + // constraint filtering mechanism. + // Collection> 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 implements Comparable> { 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 asT = (GColumnRenderer) columnRenderer; ColumnRendererMapper mapper = new ColumnRendererMapper(asT, columnBasedModel, modelIndex); Collection> rendererStringConstraints = DiscoverableTableUtils.getColumnConstraints(mapper); + // + // Case 5: the renderer supports both text filtering and column constraint filtering. + // List> 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 implements Comparable> { * This class allows us to turn client columns of type T to a String. We use * the renderer provided at construction time to generate a filter string when * {@link #convert(Object)} is called. + * + *

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 { @@ -229,14 +260,13 @@ public class ColumnFilterData implements Comparable> { @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; } - } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialogModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialogModel.java index 3c0b0891da..c89ca9195b 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialogModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialogModel.java @@ -102,9 +102,9 @@ public class ColumnFilterDialogModel { } /** - * 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) { diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/table/column/GColumnRenderer.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/table/column/GColumnRenderer.java index 35eab0293b..57d59d5011 100644 --- a/Ghidra/Framework/Docking/src/main/java/ghidra/util/table/column/GColumnRenderer.java +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/table/column/GColumnRenderer.java @@ -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.) * *

The default transformer turns items to strings by, in order,: *

    - *
  1. checking the the renderer's {@link #getFilterString(Object, Settings)}, - * if a renderer is installed + *
  2. checking the the column renderer's + * {@link #getFilterString(Object, Settings)},if a column renderer is installed *
  3. *
  4. checking to see if the column value is an instance of {@link DisplayStringProvider} *
  5. @@ -68,6 +68,10 @@ import ghidra.util.exception.AssertException; * *
* + *

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()}. + * * @param the column type */ public interface GColumnRenderer extends TableCellRenderer { diff --git a/Ghidra/Framework/Generic/src/test/java/utilities/util/reflection/ReflectionUtilitiesTest.java b/Ghidra/Framework/Generic/src/test/java/utilities/util/reflection/ReflectionUtilitiesTest.java index a2389bdf44..bc54ce7233 100644 --- a/Ghidra/Framework/Generic/src/test/java/utilities/util/reflection/ReflectionUtilitiesTest.java +++ b/Ghidra/Framework/Generic/src/test/java/utilities/util/reflection/ReflectionUtilitiesTest.java @@ -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 myList = new ArrayList() { + // stub + }; + List> types = ReflectionUtilities.getTypeArguments(List.class, myList.getClass()); + assertEquals(1, types.size()); + assertEquals(String.class, types.get(0)); + } + + @Test + public void testRumtimeTypeDiscovery_LocalVariable() { + + List myList = new ArrayList(); + List> 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> 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> 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> 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 { + // stub + } + + private interface PartiallyDefinedInterface extends RuntimeBaseInterface { + // stub + } + + private interface WhollyDefinedInterface extends RuntimeBaseInterface { + // stub + } + + private class AbstractPartiallyDefinedClass implements RuntimeBaseInterface { + // stub + } + + private class ChildExtendingPartiallyDefinedTypes + extends AbstractPartiallyDefinedClass + implements PartiallyDefinedInterface { + // stub + } + + private class ChildExtendingWhollyDefinedTypes + implements WhollyDefinedInterface { + // stub + } + + private class ChildWithMixedParentTypes + extends ArrayList + implements WhollyDefinedInterface { + // stub + } + private class RuntimeBaseType { // stub } diff --git a/Ghidra/Framework/Utility/src/main/java/utilities/util/reflection/ReflectionUtilities.java b/Ghidra/Framework/Utility/src/main/java/utilities/util/reflection/ReflectionUtilities.java index 022b87d659..d1b5a56693 100644 --- a/Ghidra/Framework/Utility/src/main/java/utilities/util/reflection/ReflectionUtilities.java +++ b/Ghidra/Framework/Utility/src/main/java/utilities/util/reflection/ReflectionUtilities.java @@ -560,15 +560,46 @@ public class ReflectionUtilities { } } + /** + * Returns the type arguments for the given base class and extension. + * + *

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: + *

+	 * 		// anonymous class definition
+	 * 		List<String> myList = new ArrayList<String>() {
+	 *			...
+	 *		};
+	 *
+	 *		// class definition
+	 *		public class MyList implements List<String> {
+	 * 
+ * + * Whereas this case will not work: + *
+	 * 		// local variable with the type specified
+	 * 		List<String> myList = new ArrayList<String>();
+	 * 
+ * + *

Note: a null entry in the result list will exist for any type that was unrecoverable + * + * + * @param the type of the base and child class + * @param baseClass the base class + * @param childClass the child class + * @return the type arguments + */ public static List> getTypeArguments(Class baseClass, Class childClass) { - Map resolvedTypesDictionary = new HashMap<>(); + Objects.requireNonNull(baseClass); + Objects.requireNonNull(childClass); + + Map 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 Type walkClassHierarchyAndResolveTypes(Class baseClass, Map 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 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 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();