diff --git a/Ghidra/Features/VersionTracking/build.gradle b/Ghidra/Features/VersionTracking/build.gradle index 9c7e973a3e..19eda022e1 100644 --- a/Ghidra/Features/VersionTracking/build.gradle +++ b/Ghidra/Features/VersionTracking/build.gradle @@ -26,6 +26,7 @@ project.ext.excludeFromParallelIntegrationTests = true dependencies { api project(":Base") + runtimeOnly project(":CodeCompare") testImplementation project(path: ':Project', configuration: 'testArtifacts') testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts') diff --git a/Ghidra/Features/VersionTracking/src/main/help/help/topics/VersionTrackingPlugin/providers/VT_Matches_Table.html b/Ghidra/Features/VersionTracking/src/main/help/help/topics/VersionTrackingPlugin/providers/VT_Matches_Table.html index 58223bb5a2..ffdba30664 100644 --- a/Ghidra/Features/VersionTracking/src/main/help/help/topics/VersionTrackingPlugin/providers/VT_Matches_Table.html +++ b/Ghidra/Features/VersionTracking/src/main/help/help/topics/VersionTrackingPlugin/providers/VT_Matches_Table.html @@ -315,9 +315,56 @@

The Clear Match action will reset the match to unaccepted and undo any applied markup.

- + +

The Remove Match - action will remove a manually created match from the matches table.

+ action will remove the selected match(es).

+ +
+

Note + + As of Ghidra 11.2, Version Tracking supports deleting matches. Any match that has + not been accepted can be deleted without confirmation. However, if you attempt to + delete an accepted match that is the last match for an association, then + you will be prompted to confirm your decision. +

+

+ Generally, we suggest users should not delete accepted matches. The more matches that + are accepted, the better the Version Tracking results, since user choices affect + future match scores. Keeping accepted matches and the applied markup provides + future analysis with more corroborating details. Contrastingly, deleting accepted + matches while keeping applied markup will remove supporting evidence that the user has + already substantiated. +

+
+

An alternative to deleting matches is to + simply filter them out of the table once they have been applied. You can also tag + any matches you wish to ignore and then use the + advanced filters to hide any matches with those tags. +

+
+

+ It is important to understand what happens in Version Tracking when deleting a match. + You will have to make a decision before deleting whether you want to keep any changes + made to the destination program when you accepted a given match or whether you wish to + remove that markup. When deleting an accepted match: +

+
+

+ To keep all applied markup, simply delete the match and, when + prompted, choose Delete Accepted Matches. This choice will delete the match and + its markup items, but any applied markup item content will remain in the destination + program. Alternatively, when prompted, you can choose Finish which will + close the prompt dialog and will not delete the remaining accepted matches or markup. +

+ To remove all applied markup, then you must first + clear the match before executing the remove action. The clear + action will remove applied markup. After clearing the match, then you can remove + the match and no markup will remain in the destination program. +

+
+ +

The Make Selections action will create selections in the source and destination tools for all matches selected in the table.

@@ -400,79 +447,101 @@

Match Filters

-
-

The match table has an extensive assortment of filters. There - are several commonly used filter controls at the bottom of the table: -

    -
  1. Text Filter - allows you to filter based on any text in the table -
  2. -
  3. Score Filter - allows you to filter on a range of scores. All scores - are between 0 and 1 -
  4. -
  5. Confidence Filter - allows you to filter a range of confidence values. - All confidence values will be greater than -9.999 and smaller than 9.999. -
  6. -
  7. Length Filter - is used to filter out - functions that are smaller than some number -
  8. -
- -

- -

Finally, the will show the ancillary filters - available. The table below lists and describes the available filters. When an ancillary - filter is applied, the icon will change to . - Further, the icon may occasionally flash as a reminder that there is a filter applied.

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ +

Table Filters

-
Filter NameDescription
Match TypeThis filter allows the user to show only function or data matches.
Association StatusThis filter allows the user to show only matches whose assocation - has one of the included status types. A useful setting - for this filter is to turn off all but the Available status. This will cause the - table to act like a "To Do" list.
Symbol TypeThis filter allows the user to show only matches whose source or - destination labels are of one of the included symbol types.
AlgorithmsThis filter allows the user to show only matches that were generated - by one of the included types of correlating algorithms
Address RangeThis filter allows the user to show only matches whose source or - destination address is within the specified range.
TagsThis filter allows the user to show only matches whose tag is an - included tag.
+
+

The match table has an extensive assortment of filters. There + are several commonly used filter controls at the bottom of the table: +

    +
  1. Text Filter - allows you to filter based on any text in the table +
  2. +
  3. Score Filter - allows you to filter on a range of scores. All scores + are between 0 and 1 +
  4. +
  5. Confidence Filter - allows you to filter a range of confidence values. + All confidence values will be greater than -9.999 and smaller than 9.999. +
  6. +
  7. Length Filter - is used to filter out + functions that are smaller than some number +
  8. +
+ +

+
+ + + +

Advanced Filters

+ +
+ +

Finally, the will show the ancillary filters + available. The table below lists and describes the available filters. When an ancillary + filter is applied, the icon will change to . + Further, the icon may occasionally flash as a reminder that there is a filter applied.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filter NameDescription
Match TypeThis filter allows the user to show only function or data matches.
Association StatusThis filter allows the user to show only matches whose assocation + has one of the included status types. A useful setting + for this filter is to turn off all but the Available status. This will cause the + table to act like a "To Do" list.
Symbol TypeThis filter allows the user to show only matches whose source or + destination labels are of one of the included symbol types.
AlgorithmsThis filter allows the user to show only matches that were generated + by one of the included types of correlating algorithms
Address RangeThis filter allows the user to show only matches whose source or + destination address is within the specified range.
TagsThis filter allows the user to show only matches whose tag is an + included tag.
+
+ +

Table Column Filters

+ +
+ The matches table also supports + + Table Column Filters for creating complex filters for individual table columns. +
+
diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/AssociationDatabaseManager.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/AssociationDatabaseManager.java index ed9198bbe3..ecbdc67f83 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/AssociationDatabaseManager.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/AssociationDatabaseManager.java @@ -21,8 +21,7 @@ import java.io.IOException; import java.util.*; import db.*; -import ghidra.feature.vt.api.impl.MarkupItemStorage; -import ghidra.feature.vt.api.impl.VTEvent; +import ghidra.feature.vt.api.impl.*; import ghidra.feature.vt.api.main.*; import ghidra.feature.vt.api.util.VTAssociationStatusException; import ghidra.framework.data.OpenMode; @@ -138,33 +137,13 @@ public class AssociationDatabaseManager implements VTAssociationManager { return createMarkupItemDB(markupItemStorage); } - void removeMarkupItem(MarkupItemStorageDB appliedMarkupItemDB) { + // non-interface method; internal API use + public void removeStoredMarkupItems(List impls) { - VTAssociationDB association = (VTAssociationDB) appliedMarkupItemDB.getAssociation(); - - validateAcceptedState(appliedMarkupItemDB, association); - - try { - markupItemTableAdapter.removeMatchMarkupItemRecord(appliedMarkupItemDB.getKey()); - } - catch (IOException e) { - session.dbError(e); - } - } - - private void validateAcceptedState(MarkupItemStorageDB appliedItem, - VTAssociationDB association) { - // - // For any 'applied' markup item we assume that its association will be 'ACCEPTED'. The - // exception to this rule is when we have markup items in the database, but that are not - // applied (like when we change the destination address without applying) - // - VTAssociationStatus associationStatus = association.getStatus(); - VTMarkupItemStatus status = appliedItem.getStatus(); - if (status.isUnappliable()) { - if (associationStatus != ACCEPTED) { - throw new AssertException("Cannot have an applied markup item with an " + - "association that is not ACCEPTED"); + for (MarkupItemImpl impl : impls) { + MarkupItemStorage storage = impl.getStorage(); + if (storage instanceof MarkupItemStorageDB storageDb) { + removeMarkupRecord(storageDb.getKey()); } } } @@ -251,17 +230,31 @@ public class AssociationDatabaseManager implements VTAssociationManager { } void removeAssociation(VTAssociation association) { - VTAssociationDB existingAssociation = (VTAssociationDB) association; - long id = existingAssociation.getKey(); + + // Update the association status so that we update any blocked associations + VTAssociationDB associationDB = (VTAssociationDB) association; + VTAssociationStatus status = association.getStatus(); + if (status == ACCEPTED) { + associationDB.setStatus(AVAILABLE); + associationDB.setInvalid(); + unblockRelatedAssociations(associationDB); + for (AssociationHook hook : associationHooks) { + hook.associationCleared(associationDB); + } + } + + VTAssociationDB associationDb = (VTAssociationDB) association; + long id = associationDb.getKey(); try { + associationDb.removeMarkupItems(); associationTableAdapter.removeAssociaiton(id); - session.setChanged(VTEvent.ASSOCIATION_REMOVED, existingAssociation, null); + session.setChanged(VTEvent.ASSOCIATION_REMOVED, associationDb, null); } catch (IOException e) { session.dbError(e); } associationCache.delete(id); - existingAssociation.setInvalid(); + associationDb.setInvalid(); } @@ -507,7 +500,7 @@ public class AssociationDatabaseManager implements VTAssociationManager { throws VTAssociationStatusException { if (association.hasAppliedMarkupItems()) { throw new VTAssociationStatusException( - "VTMarkupItemManager contains applied " + "markup items"); + "VTMarkupItemManager contains applied markup items"); } } @@ -623,9 +616,9 @@ public class AssociationDatabaseManager implements VTAssociationManager { associationHooks.remove(hook); } - void removeMarkupRecord(DBRecord record) { + void removeMarkupRecord(long key) { try { - markupItemTableAdapter.removeMatchMarkupItemRecord(record.getKey()); + markupItemTableAdapter.removeMarkupItemRecord(key); } catch (IOException e) { session.dbError(e); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/MarkupItemStorageDB.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/MarkupItemStorageDB.java index 7306cd44bc..f85df416e4 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/MarkupItemStorageDB.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/MarkupItemStorageDB.java @@ -135,7 +135,7 @@ public class MarkupItemStorageDB extends DatabaseObject implements MarkupItemSto try { MarkupItemStorage storage = new MarkupItemStorageImpl(getAssociation(), getMarkupType(), getSourceAddress(), getDestinationAddress(), getDestinationAddressSource()); - associationManager.removeMarkupRecord(record); + associationManager.removeMarkupRecord(record.getKey()); return storage; } finally { @@ -144,7 +144,8 @@ public class MarkupItemStorageDB extends DatabaseObject implements MarkupItemSto } @Override - public MarkupItemStorage setDestinationAddress(Address destinationAddress, String addressSource) { + public MarkupItemStorage setDestinationAddress(Address destinationAddress, + String addressSource) { if (destinationAddress == null) { destinationAddress = Address.NO_ADDRESS; } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTAssociationDB.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTAssociationDB.java index 5b1ee2a161..a046e5795c 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTAssociationDB.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTAssociationDB.java @@ -296,4 +296,8 @@ public class VTAssociationDB extends DatabaseObject implements VTAssociation { public boolean hasAppliedMarkupItems() { return markupManager.hasAppliedMarkupItems(); } + + void removeMarkupItems() { + markupManager.removeMarkupItems(); + } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchMarkupItemTableDBAdapter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchMarkupItemTableDBAdapter.java index e350832520..c9c986b465 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchMarkupItemTableDBAdapter.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchMarkupItemTableDBAdapter.java @@ -60,7 +60,7 @@ public abstract class VTMatchMarkupItemTableDBAdapter { public abstract RecordIterator getRecords() throws IOException; - public abstract void removeMatchMarkupItemRecord(long key) throws IOException; + public abstract void removeMarkupItemRecord(long key) throws IOException; public abstract DBRecord getRecord(long key) throws IOException; @@ -70,5 +70,6 @@ public abstract class VTMatchMarkupItemTableDBAdapter { public abstract int getRecordCount(); - public abstract DBRecord createMarkupItemRecord(MarkupItemStorage markupItem) throws IOException; + public abstract DBRecord createMarkupItemRecord(MarkupItemStorage markupItem) + throws IOException; } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchMarkupItemTableDBAdapterV0.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchMarkupItemTableDBAdapterV0.java index 0e2d687519..33f143193b 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchMarkupItemTableDBAdapterV0.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchMarkupItemTableDBAdapterV0.java @@ -15,14 +15,11 @@ */ package ghidra.feature.vt.api.db; -import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.ADDRESS_SOURCE_COL; -import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.ASSOCIATION_KEY_COL; -import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.DESTINATION_ADDRESS_COL; -import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.MARKUP_TYPE_COL; -import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.ORIGINAL_DESTINATION_VALUE_COL; -import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.SOURCE_ADDRESS_COL; -import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.SOURCE_VALUE_COL; -import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.STATUS_COL; +import static ghidra.feature.vt.api.db.VTMatchMarkupItemTableDBAdapter.MarkupTableDescriptor.*; + +import java.io.IOException; + +import db.*; import ghidra.feature.vt.api.impl.MarkupItemStorage; import ghidra.feature.vt.api.main.VTSession; import ghidra.feature.vt.api.markuptype.VTMarkupTypeFactory; @@ -34,10 +31,6 @@ import ghidra.program.model.listing.Program; import ghidra.util.exception.VersionException; import ghidra.util.task.TaskMonitor; -import java.io.IOException; - -import db.*; - public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBAdapter { private Table table; @@ -71,20 +64,20 @@ public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBA record.setLongValue(ASSOCIATION_KEY_COL.column(), association.getKey()); record.setString(ADDRESS_SOURCE_COL.column(), markupItem.getDestinationAddressSource()); - record.setLongValue(SOURCE_ADDRESS_COL.column(), getAddressID(sourceProgram, - markupItem.getSourceAddress())); + record.setLongValue(SOURCE_ADDRESS_COL.column(), + getAddressID(sourceProgram, markupItem.getSourceAddress())); Address destinationAddress = markupItem.getDestinationAddress(); if (destinationAddress != null) { - record.setLongValue(DESTINATION_ADDRESS_COL.column(), getAddressID(destinationProgram, - markupItem.getDestinationAddress())); + record.setLongValue(DESTINATION_ADDRESS_COL.column(), + getAddressID(destinationProgram, markupItem.getDestinationAddress())); } record.setShortValue(MARKUP_TYPE_COL.column(), (short) VTMarkupTypeFactory.getID(markupItem.getMarkupType())); - record.setString(SOURCE_VALUE_COL.column(), Stringable.getString( - markupItem.getSourceValue(), sourceProgram)); - record.setString(ORIGINAL_DESTINATION_VALUE_COL.column(), Stringable.getString( - markupItem.getDestinationValue(), destinationProgram)); + record.setString(SOURCE_VALUE_COL.column(), + Stringable.getString(markupItem.getSourceValue(), sourceProgram)); + record.setString(ORIGINAL_DESTINATION_VALUE_COL.column(), + Stringable.getString(markupItem.getDestinationValue(), destinationProgram)); record.setByteValue(STATUS_COL.column(), (byte) markupItem.getStatus().ordinal()); table.putRecord(record); @@ -97,7 +90,7 @@ public class VTMatchMarkupItemTableDBAdapterV0 extends VTMatchMarkupItemTableDBA } @Override - public void removeMatchMarkupItemRecord(long key) throws IOException { + public void removeMarkupItemRecord(long key) throws IOException { table.deleteRecord(key); } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchSetDB.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchSetDB.java index 17e44357b3..fb97a0f858 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchSetDB.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/db/VTMatchSetDB.java @@ -25,8 +25,6 @@ import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import db.*; -import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator; -import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator; import ghidra.feature.vt.api.impl.*; import ghidra.feature.vt.api.main.*; import ghidra.framework.data.OpenMode; @@ -186,37 +184,47 @@ public class VTMatchSetDB extends DatabaseObject implements VTMatchSet { @Override public boolean removeMatch(VTMatch match) { - if (!(match instanceof VTMatchDB)) { - return false; - } - if (!match.getMatchSet().hasRemovableMatches()) { - return false; - } - VTMatchDB matchDB = (VTMatchDB) match; + if (!(match instanceof VTMatchDB matchDb)) { + // this should not be possible from the UI + throw new IllegalArgumentException("Can only remove matches saved to the database"); + } VTAssociation association = match.getAssociation(); - - // Remove the association if it was the only remaining match for that association. - AssociationDatabaseManager associationManager = session.getAssociationManagerDBM(); List matches = session.getMatches(association); if (matches.size() == 1 && association.getStatus() == VTAssociationStatus.ACCEPTED) { - return false; // can't remove the last match if the association is accepted + // This method prevents deleting the association if it is accepted, as it would cause + // the user to lose potentially valuable information without realizing it. To work + // around that issue when calling this method, the user can first un-accept the match. + return false; } - // Remove the match record + deleteMatch(matchDb); + return true; + } + + @Override + public void deleteMatch(VTMatch match) { + if (!(match instanceof VTMatchDB matchDb)) { + // this should not be possible from the UI + throw new IllegalArgumentException("Can only remove matches saved to the database"); + } + + VTAssociation association = match.getAssociation(); Address sourceAddress = association.getSourceAddress(); Address destinationAddress = association.getDestinationAddress(); try { lock.acquire(); - long matchKey = matchDB.getKey(); + long matchKey = matchDb.getKey(); boolean deleted = matchTableAdapter.deleteRecord(matchKey); if (deleted) { matchCache.delete(matchKey); - if (matches.size() == 1) { + List matches = session.getMatches(association); + if (matches.isEmpty()) { // if last match, remove association - associationManager.removeAssociation(association); + AssociationDatabaseManager manager = session.getAssociationManagerDBM(); + manager.removeAssociation(association); } } } @@ -229,7 +237,6 @@ public class VTMatchSetDB extends DatabaseObject implements VTMatchSet { DeletedMatch deletedMatch = new DeletedMatch(sourceAddress, destinationAddress); session.setObjectChanged(VTEvent.MATCH_DELETED, match, deletedMatch, null); - return true; } @Override @@ -349,14 +356,6 @@ public class VTMatchSetDB extends DatabaseObject implements VTMatchSet { } } - @Override - public boolean hasRemovableMatches() { - VTProgramCorrelatorInfo info = getProgramCorrelatorInfo(); - String correlatorClassName = info.getCorrelatorClassName(); - return correlatorClassName.equals(ManualMatchProgramCorrelator.class.getName()) || - correlatorClassName.equals(ImpliedMatchProgramCorrelator.class.getName()); - } - @Override public String toString() { return "Match Set " + getID() + " - " + getMatchCount() + " matches [Correlator=" + diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MarkupItemImpl.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MarkupItemImpl.java index 8cce247561..90c536c950 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MarkupItemImpl.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MarkupItemImpl.java @@ -501,4 +501,8 @@ public class MarkupItemImpl implements VTMarkupItem { newStatus); } + // non-interface method + public MarkupItemStorage getStorage() { + return markupItemStorage; + } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MarkupItemManagerImpl.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MarkupItemManagerImpl.java index d1f1778b69..3bfc49dbb9 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MarkupItemManagerImpl.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MarkupItemManagerImpl.java @@ -15,14 +15,14 @@ */ package ghidra.feature.vt.api.impl; +import java.util.*; +import java.util.stream.Collectors; + import ghidra.feature.vt.api.db.*; import ghidra.feature.vt.api.main.VTMarkupItem; import ghidra.feature.vt.api.main.VTMarkupItemStatus; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -import ghidra.util.task.TaskMonitorAdapter; - -import java.util.*; public class MarkupItemManagerImpl { @@ -70,7 +70,7 @@ public class MarkupItemManagerImpl { return Collections.unmodifiableList(markupItems); } - protected List createMarkupItems(TaskMonitor monitor) throws CancelledException { + private List createMarkupItems(TaskMonitor monitor) throws CancelledException { Collection generatedMarkupItems = getGeneratedMarkupItems(monitor); Collection databaseMarkupItems = getStoredMarkupItems(monitor); @@ -104,18 +104,6 @@ public class MarkupItemManagerImpl { return false; } - private Collection getStoredMarkupItems(TaskMonitor monitor) - throws CancelledException { - AssociationDatabaseManager associationDBM = association.getAssociationManagerDB(); - Collection appliedMarkupItems = - associationDBM.getAppliedMarkupItems(monitor, association); - List list = new ArrayList(); - for (MarkupItemStorageDB markupItemStorageDB : appliedMarkupItems) { - list.add(new MarkupItemImpl(markupItemStorageDB)); - } - return list; - } - private List replaceGeneratedMarkupItemsWithDBMarkupItems( Collection generatedMarkupItems, Collection databaseMarkupItems) { @@ -142,8 +130,45 @@ public class MarkupItemManagerImpl { markupItem.getSourceAddress().toString(true); } - public void clearCache() { + // synchronized due to write of 'markupItems' + public synchronized void clearCache() { markupItems = EMPTY_LIST; } + private Collection getStoredMarkupItems(TaskMonitor monitor) + throws CancelledException { + AssociationDatabaseManager associationDBM = association.getAssociationManagerDB(); + Collection appliedMarkupItems = + associationDBM.getAppliedMarkupItems(monitor, association); + List list = new ArrayList(); + for (MarkupItemStorageDB markupItemStorageDB : appliedMarkupItems) { + list.add(new MarkupItemImpl(markupItemStorageDB)); + } + return list; + } + + // synchronized to match getMarkupItems() so we do not have other clients loading items while + // we are processing them + public synchronized void removeMarkupItems() { + + List items; + try { + items = getMarkupItems(TaskMonitor.DUMMY); + } + catch (CancelledException e) { + return; // can't happen with DUMMY + } + + List impls = items.stream() + .map(item -> (MarkupItemImpl) item) + .filter(impl -> impl.isStoredInDB()) + .collect(Collectors.toList()); + + AssociationDatabaseManager associationDbm = association.getAssociationManagerDB(); + associationDbm.removeStoredMarkupItems(impls); + + // signal that markup item info has changed and must be reloaded when next needed + clearCache(); + } + } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MatchSetImpl.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MatchSetImpl.java index 5c5d28ac5c..6e4124809f 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MatchSetImpl.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/impl/MatchSetImpl.java @@ -15,13 +15,11 @@ */ package ghidra.feature.vt.api.impl; -import ghidra.feature.vt.api.correlator.program.ImpliedMatchProgramCorrelator; -import ghidra.feature.vt.api.correlator.program.ManualMatchProgramCorrelator; +import java.util.*; + import ghidra.feature.vt.api.main.*; import ghidra.program.model.address.Address; -import java.util.*; - public class MatchSetImpl implements VTMatchSet { private ProgramCorrelatorInfoFake correlatorInfo; private final VTSession session; @@ -77,11 +75,8 @@ public class MatchSetImpl implements VTMatchSet { } @Override - public boolean hasRemovableMatches() { - VTProgramCorrelatorInfo info = getProgramCorrelatorInfo(); - String correlatorClassName = info.getCorrelatorClassName(); - return correlatorClassName.equals(ManualMatchProgramCorrelator.class.getName()) || - correlatorClassName.equals(ImpliedMatchProgramCorrelator.class.getName()); + public void deleteMatch(VTMatch match) { + throw new UnsupportedOperationException(); } @Override diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/main/VTMatchSet.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/main/VTMatchSet.java index 84a6b4284d..15e09e7e01 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/main/VTMatchSet.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/api/main/VTMatchSet.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +15,11 @@ */ package ghidra.feature.vt.api.main; +import java.util.Collection; + import ghidra.feature.vt.api.impl.VTProgramCorrelatorInfo; import ghidra.program.model.address.Address; -import java.util.Collection; - /** * Interface for all the matches generated from a single program correlator run. * @@ -57,14 +56,14 @@ public interface VTMatchSet { /** * Returns the number of matches contained in this match set. - * @return + * @return the number of matches contained in this match set. */ public int getMatchCount(); /** * Returns a unique id for this match set. The ids are one-up numbers indicating the order this * match set was generated in relation to other match sets in the VTSession. - * @return + * @return the id */ public int getID(); @@ -72,7 +71,7 @@ public interface VTMatchSet { * Returns a collection of all matches for the given association. * @param association the association for which to search for matches. * @return a collection of all matches for the given association. - * @see #getMatches(Address, Address, VTAssociationType) + * @see #getMatches(Address, Address) */ public Collection getMatches(VTAssociation association); @@ -90,16 +89,51 @@ public interface VTMatchSet { public Collection getMatches(Address sourceAddress, Address destinationAddress); /** - * Removes a match from this match set. Note that this operation is only supported for built-in - * match sets "Manual Matches" and "Implied Matches". + * Deletes the given match from this match set. + *

+ * Note: deleting an ACCEPTED match removes potentially useful corroborating evidence + * from future correlation. Before deleting a match, consider instead filtering matches out of + * the UI that you are finished applying. + *

+ * If this is the last match that shares the match's association, then the association will also + * be removed, along with any markup items in the database. Any applied markup item data + * will not be changed. + * + * @param match the match + */ + public void deleteMatch(VTMatch match); + + /** + * Removes a match from this match set. + *

+ * If this is the last match that shares the match's association, then the match will only be + * removed if the association is not accepted. In that case, no remove will take place and + * this method will return false. + *

+ * Note: This method is deprecated. It unfortunately shares a very similar name with its + * replacement, {@link #deleteMatch(VTMatch)}. The replacement method will delete the match + * and the related association and markup items in the database, if the match is the last match + * to use that association. This deprecated method does not remove the remaining association or + * markup items. Historically, this method has been called after clearing the given match and + * its markup. Once this method has been deleted, clients will be responsible for managing the + * markup item state before calling {@link #deleteMatch(VTMatch)}. + * * @param match the match to remove. * @return true if the match was removed. + * @throws IllegalArgumentException if a non-database match is passed to this method + * @see #deleteMatch(VTMatch) + * @deprecated use {@link #deleteMatch(VTMatch)} */ + @Deprecated(since = "11.2", forRemoval = true) public boolean removeMatch(VTMatch match); /** - * Returns true if this match set supports removing matches. - * @return true if this match set supports removing matches. + * Returns true + * @return true + * @deprecated this method now always returns true */ - public boolean hasRemovableMatches(); + @Deprecated(since = "11.2", forRemoval = true) + public default boolean hasRemovableMatches() { + return true; + } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/RemoveMatchAction.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/RemoveMatchAction.java index 77483d07d7..62f7dd0173 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/RemoveMatchAction.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/actions/RemoveMatchAction.java @@ -41,7 +41,6 @@ public class RemoveMatchAction extends DockingAction { super("Remove", VTPlugin.OWNER); this.controller = controller; -// setToolBarData(new ToolBarData(ICON, MENU_GROUP)); setPopupMenuData(new MenuData(new String[] { "Remove Match" }, ICON, MENU_GROUP)); setEnabled(false); setHelpLocation(new HelpLocation("VersionTrackingPlugin", "Remove_Match")); @@ -64,17 +63,7 @@ public class RemoveMatchAction extends DockingAction { } VTMatchContext matchContext = (VTMatchContext) context; List matches = matchContext.getSelectedMatches(); - if (matches.size() == 0) { - return false; - } - if (!isRemovableMatch(matches.get(0))) { - return false; // It must be a single manual match. - } - return true; - } - - private boolean isRemovableMatch(VTMatch vtMatch) { - return vtMatch.getMatchSet().hasRemovableMatches(); + return !matches.isEmpty(); } @Override diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/AbstractAddressRangeFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/AbstractAddressRangeFilter.java index 99b2d93da5..d32197ce80 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/AbstractAddressRangeFilter.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/AbstractAddressRangeFilter.java @@ -282,12 +282,6 @@ public abstract class AbstractAddressRangeFilter extends AncillaryFilter fireStatusChanged(getFilterStatus()); } - @Override - public void clearFilter() { - lowerAddressRangeTextField.setText(MIN_ADDRESS_VALUE.toString()); - upperAddressRangeTextField.setText(MAX_ADDRESS_VALUE.toString()); - } - @Override public JComponent getComponent() { return component; diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/CheckBoxBasedAncillaryFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/CheckBoxBasedAncillaryFilter.java index f14f0c08a5..afcf350500 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/CheckBoxBasedAncillaryFilter.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/CheckBoxBasedAncillaryFilter.java @@ -15,8 +15,7 @@ */ package ghidra.feature.vt.gui.filters; -import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.APPLIED; -import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.NONE; +import static ghidra.feature.vt.gui.filters.Filter.FilterEditingStatus.*; import java.awt.Container; import java.awt.LayoutManager; @@ -151,13 +150,6 @@ public abstract class CheckBoxBasedAncillaryFilter extends AncillaryFilter return FilterShortcutState.REQUIRES_CHECK; } - @Override - public void clearFilter() { - for (CheckBoxInfo info : checkBoxInfos) { - info.setSelected(true); - } - } - @Override public FilterState getFilterState() { FilterState state = new FilterState(this); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/Filter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/Filter.java index 32c3b177b3..6b37eee795 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/Filter.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/Filter.java @@ -46,8 +46,6 @@ public abstract class Filter { public abstract FilterEditingStatus getFilterStatus(); - public abstract void clearFilter(); - public abstract JComponent getComponent(); public void dispose() { diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/FilterDialogModel.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/FilterDialogModel.java index 3c61b602bb..2eb3325286 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/FilterDialogModel.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/FilterDialogModel.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +17,13 @@ package ghidra.feature.vt.gui.filters; public interface FilterDialogModel { - public void addFilter( Filter filter ); - - public void forceRefilter(); - - /** - * Will be called when the visibility of the dialog using this model has changed - */ - public void dialogVisibilityChanged( boolean isVisible ); + public void addFilter(Filter filter); + + public void forceRefilter(); + + /** + * Will be called when the visibility of the dialog using this model has changed + * @param isVisible true if visible + */ + public void dialogVisibilityChanged(boolean isVisible); } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/TagFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/TagFilter.java index ed185935ae..8544644aba 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/TagFilter.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/filters/TagFilter.java @@ -243,12 +243,6 @@ public class TagFilter extends AncillaryFilter { excludedTags = getTagsFromText(tagText); } - @Override - public void clearFilter() { - excludedTags.clear(); - excludedTagsLabel.setText(ALL_TAGS_INCLUDED); - } - @Override public JComponent getComponent() { return component; diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java index 584b27f9b9..eff11789d7 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/plugin/VTPlugin.java @@ -150,13 +150,14 @@ public class VTPlugin extends Plugin { private void addCustomPlugins() { - List names = new ArrayList<>(List.of("ghidra.features.codecompare.plugin")); + List names = + new ArrayList<>(List.of("ghidra.features.codecompare.plugin.FunctionComparisonPlugin")); List plugins = tool.getManagedPlugins(); Set existingNames = plugins.stream().map(c -> c.getName()).collect(Collectors.toSet()); // Note: we check to see if the plugins we want to add have already been added to the tool. - // We should not needed to do this, but once the tool has been saved with the plugins added, + // We should not need to do this, but once the tool has been saved with the plugins added, // they will get added again the next time the tool is loaded. Adding this check here seems // easier than modifying the default to file to load the plugins, since the amount of xml // required for that is non-trivial. diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/AbstractDoubleRangeFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/AbstractDoubleRangeFilter.java index df31d03c17..158933b416 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/AbstractDoubleRangeFilter.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/AbstractDoubleRangeFilter.java @@ -127,12 +127,6 @@ public abstract class AbstractDoubleRangeFilter extends Filter return component; } - @Override - public void clearFilter() { - lowerBoundField.setText(minValue.toString()); - upperBoundField.setText(maxValue.toString()); - } - @Override public FilterEditingStatus getFilterStatus() { FilterEditingStatus lowerStatus = lowerBoundField.getFilterStatus(); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/LengthFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/LengthFilter.java index cca9d877b8..6114e3c967 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/LengthFilter.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/LengthFilter.java @@ -89,11 +89,6 @@ public class LengthFilter extends Filter { return component; } - @Override - public void clearFilter() { - textField.setText(DEFAULT_FILTER_VALUE.toString()); - } - @Override public FilterEditingStatus getFilterStatus() { return textField.getFilterStatus(); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/VTMatchTableProvider.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/VTMatchTableProvider.java index fff753a973..43b4986444 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/VTMatchTableProvider.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/provider/matchtable/VTMatchTableProvider.java @@ -34,6 +34,8 @@ import javax.swing.table.*; import docking.*; import docking.action.builder.ActionBuilder; import docking.widgets.table.*; +import docking.widgets.table.columnfilter.ColumnBasedTableFilter; +import docking.widgets.table.columnfilter.ColumnFilterManager; import docking.widgets.table.threaded.ThreadedTableModel; import generic.theme.GIcon; import ghidra.app.services.FunctionComparisonService; @@ -80,6 +82,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter private AncillaryFilterDialogComponentProvider ancillaryFilterDialog; private JButton ancillaryFilterButton; + private ColumnFilterManager columnFilterManager; + private VTColumnFilter vtColumnFilter; private FilterIconFlashTimer iconTimer; private Set> filters = new HashSet<>(); @@ -386,8 +390,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter JPanel innerPanel = new JPanel(new HorizontalLayout(4)); innerPanel.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 4)); - JComponent nameFilterPanel = createTextFilterPanel(); - parentPanel.add(nameFilterPanel, BorderLayout.CENTER); + JComponent textFilterPanel = createTextFilterPanel(); + parentPanel.add(textFilterPanel, BorderLayout.CENTER); parentPanel.add(innerPanel, BorderLayout.EAST); JComponent scoreFilterPanel = createScoreFilterPanel(); @@ -409,13 +413,33 @@ public class VTMatchTableProvider extends ComponentProviderAdapter helpService.registerHelp(parentPanel, filterHelpLocation); helpService.registerHelp(ancillaryFilterButton, filterHelpLocation); + JButton columnFilterButton = createColumnFilterButton(); + innerPanel.add(columnFilterButton); + innerPanel.add(ancillaryFilterButton); return parentPanel; } + private JButton createColumnFilterButton() { + + String preferenceKey = + matchesTable.getPreferenceKey() + ColumnFilterManager.FILTER_EXTENSION; + columnFilterManager = new ColumnFilterManager(matchesTable, matchesTableModel, + preferenceKey, this::updateColumnFilter); + + vtColumnFilter = new VTColumnFilter(columnFilterManager.getCurrentFilter()); + addFilter(vtColumnFilter); + + return columnFilterManager.getConfigureButton(); + } + + private void updateColumnFilter() { + vtColumnFilter.setFilter(columnFilterManager.getCurrentFilter()); + refilter(); + } + private JComponent createTextFilterPanel() { -// MatchNameFilter nameFilterPanel = new MatchNameFilter(controller, matchesTable); AllTextFilter allTextFilter = new AllTextFilter<>(controller, matchesTable, matchesTableModel); allTextFilter.setName(TEXT_FILTER_NAME); @@ -506,6 +530,8 @@ public class VTMatchTableProvider extends ComponentProviderAdapter } ancillaryFilterDialog.dispose(); + + columnFilterManager.dispose(); } @Override @@ -1069,4 +1095,73 @@ public class VTMatchTableProvider extends ComponentProviderAdapter return list; } } + + private class VTColumnFilter extends Filter { + + private ColumnBasedTableFilter columnFilter; + + VTColumnFilter(ColumnBasedTableFilter columnFilter) { + this.columnFilter = columnFilter; + } + + void setFilter(ColumnBasedTableFilter columnFilter) { + this.columnFilter = columnFilter; + } + + @Override + public boolean passesFilter(VTMatch t) { + if (columnFilter == null) { + return true; + } + return columnFilter.acceptsRow(t); + } + + @Override + public FilterEditingStatus getFilterStatus() { + if (columnFilter == null || columnFilter.isEmpty()) { + return FilterEditingStatus.NONE; + } + + return FilterEditingStatus.APPLIED; + } + + @Override + public JComponent getComponent() { + // This filter is configured outside of the VT filter API + throw new UnsupportedOperationException(); + } + + @Override + public FilterShortcutState getFilterShortcutState() { + return FilterShortcutState.REQUIRES_CHECK; + } + + @Override + public Filter createCopy() { + return this; // does not currently support copying; should not be needed + } + + @Override + public void readConfigState(SaveState saveState) { + // handled by the column filter manager + } + + @Override + public void writeConfigState(SaveState saveState) { + // handled by the column filter manager + } + + @Override + public boolean isSubFilterOf(Filter otherFilter) { + if (columnFilter == null) { + return false; + } + if (otherFilter instanceof VTColumnFilter otherColumnFilter) { + return columnFilter.isSubFilterOf(otherColumnFilter.columnFilter); + } + return false; + } + + } + } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/task/RemoveMatchTask.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/task/RemoveMatchTask.java index 3540fdddb0..1438d1dfc4 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/task/RemoveMatchTask.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/task/RemoveMatchTask.java @@ -15,11 +15,13 @@ */ package ghidra.feature.vt.gui.task; -import java.util.List; +import java.util.*; +import docking.widgets.OptionDialog; import ghidra.feature.vt.api.db.VTMatchSetDB; import ghidra.feature.vt.api.main.VTMatch; import ghidra.feature.vt.api.main.VTSession; +import ghidra.util.HelpLocation; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; @@ -38,26 +40,75 @@ public class RemoveMatchTask extends VtTask { return true; } - private boolean removeMatches(TaskMonitor monitor) throws CancelledException { + private void removeMatches(TaskMonitor monitor) throws CancelledException { + monitor.setMessage("Removing matches"); - monitor.initialize(matches.size()); - boolean failed = false; - for (VTMatch match : matches) { + int n = matches.size(); + monitor.initialize(n); + + // + // First remove all matches that will not require user prompting (those that are not + // accepted or they are not the last match for a shared association). + // + List list = new ArrayList<>(matches); // create a mutable list + Iterator it = list.iterator(); + while (it.hasNext()) { monitor.checkCancelled(); + VTMatch match = it.next(); VTMatchSetDB matchSet = (VTMatchSetDB) match.getMatchSet(); - boolean matchRemoved = matchSet.removeMatch(match); - if (!matchRemoved) { - failed = true; + if (matchSet.removeMatch(match)) { + it.remove(); } monitor.incrementProgress(1); } - monitor.setProgress(matches.size()); - if (failed) { - reportError("One or more of your matches could not be removed." + - "\nNote: You can't remove a match if it is currently accepted."); + if (list.isEmpty()) { + return; + } + + // + // Now we have to ask the user if they wish to remove applied matches. + // + int delta = n - list.size(); + + //@formatter:off + String message = """ + Deleted %d of %d matches. + + The remaining %d matches are ACCEPTED. Do you wish to delete these matches and + leave any applied destination program markup in place? + (Press F1 to see more help details) + """.formatted(delta, n, list.size()); + //@formatter:on + + RemoveMatchDialog dialog = new RemoveMatchDialog(message); + if (!dialog.promptToDelete()) { + return; + } + + it = list.iterator(); + while (it.hasNext()) { + monitor.checkCancelled(); + VTMatch match = it.next(); + VTMatchSetDB matchSet = (VTMatchSetDB) match.getMatchSet(); + matchSet.deleteMatch(match); + it.remove(); + monitor.incrementProgress(1); } - return true; } + private class RemoveMatchDialog extends OptionDialog { + + RemoveMatchDialog(String message) { + super("Delete ACCEPTED Matches?", message, "Delete Accepted Matches", "Finish", + OptionDialog.QUESTION_MESSAGE, null, false); + + setHelpLocation(new HelpLocation("VersionTrackingPlugin", "Remove_Match")); + } + + boolean promptToDelete() { + int choice = super.show(); + return choice == OptionDialog.OPTION_ONE; // "Delete Accepted Matches" + } + } } diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractTextFilter.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractTextFilter.java index 78fbaf8ca2..443ec455e6 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractTextFilter.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/AbstractTextFilter.java @@ -125,11 +125,6 @@ public abstract class AbstractTextFilter extends Filter { return component; } - @Override - public void clearFilter() { - textField.setText(defaultValue); - } - @Override public FilterEditingStatus getFilterStatus() { return textField.getFilterStatus(); diff --git a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/ImpliedMatchUtils.java b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/ImpliedMatchUtils.java index 57d3f69dc3..592daa6a57 100644 --- a/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/ImpliedMatchUtils.java +++ b/Ghidra/Features/VersionTracking/src/main/java/ghidra/feature/vt/gui/util/ImpliedMatchUtils.java @@ -115,7 +115,7 @@ public class ImpliedMatchUtils { VTMatchSet impliedMatchSet = session.getImpliedMatchSet(); for (VTMatch vtMatch : matches) { if (vtMatch.getMatchSet() == impliedMatchSet) { - impliedMatchSet.removeMatch(vtMatch); + impliedMatchSet.deleteMatch(vtMatch); } } } @@ -123,8 +123,7 @@ public class ImpliedMatchUtils { /** * Method for finding version tracking implied matches given an accepted matched * function. Each referenced data and function that exist in equivalent sections - * of the matched source and destination functions will added to the current - * version tracking session as an implied match. + * of the matched source and destination functions will be returned in the given set. * * @param sourceFunction The matched function from the source program * @param destinationFunction The matched function from the destination program diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java index 40d6046b74..bc17c2ea38 100644 --- a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java +++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchAcceptTest.java @@ -18,7 +18,7 @@ package ghidra.feature.vt.api; import static ghidra.feature.vt.db.VTTestUtils.*; import static org.junit.Assert.*; -import java.util.*; +import java.util.Arrays; import org.junit.*; @@ -27,9 +27,8 @@ import ghidra.feature.vt.api.main.VTAssociationStatus; import ghidra.feature.vt.api.main.VTMatch; import ghidra.feature.vt.gui.plugin.*; import ghidra.feature.vt.gui.task.AcceptMatchTask; +import ghidra.feature.vt.gui.task.VtTask; import ghidra.feature.vt.gui.util.VTOptionDefines; -import ghidra.framework.model.DomainObjectChangedEvent; -import ghidra.framework.model.DomainObjectListener; import ghidra.framework.options.Options; import ghidra.framework.plugintool.PluginTool; import ghidra.program.database.ProgramDB; @@ -39,10 +38,7 @@ import ghidra.program.model.listing.*; import ghidra.program.model.symbol.*; import ghidra.program.model.util.CodeUnitInsertionException; import ghidra.test.*; -import ghidra.util.exception.CancelledException; import ghidra.util.exception.InvalidInputException; -import ghidra.util.task.Task; -import ghidra.util.task.TaskMonitor; public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest { @@ -53,13 +49,8 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest { private ProgramDB sourceProgram; private ProgramDB destinationProgram; private VTPlugin plugin; - private DomainObjectListenerRecorder eventRecorder = new DomainObjectListenerRecorder(); private Options options; - public VTMatchAcceptTest() { - super(); - } - @Before public void setUp() throws Exception { @@ -70,7 +61,6 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest { ClassicSampleX86ProgramBuilder destinationBuilder = new ClassicSampleX86ProgramBuilder(); destinationProgram = destinationBuilder.getProgram(); - destinationProgram.addListener(eventRecorder); tool = env.getTool(); @@ -84,29 +74,21 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest { runSwing(() -> controller.openVersionTrackingSession(session)); options = controller.getOptions(); - options.setBoolean(VTOptionDefines.AUTO_CREATE_IMPLIED_MATCH, false); - options.setBoolean(VTOptionDefines.APPLY_FUNCTION_NAME_ON_ACCEPT, false); - options.setBoolean(VTOptionDefines.APPLY_DATA_NAME_ON_ACCEPT, false); } @After public void tearDown() throws Exception { - waitForBusyTool(tool); - destinationProgram.flushEvents(); - waitForSwing(); - env.dispose(); - } @Test public void testAcceptWithApplyDataLabels() throws Exception { // - // BTW this test exposes a bug because the hook that runs when you apply data on accept was. - // in a side effect, causing the destination address to be set. When the hook was changed to - // not set the destination address, the accept task was not setting the destination address - // as it should. + // This test exposes a bug because the hook that runs when you apply data on accept was + // exhibiting a side effect, causing the destination address to be set. When the hook was + // changed to not set the destination address, the accept task was not setting the + // destination address as it should. // options.setBoolean(VTOptionDefines.APPLY_DATA_NAME_ON_ACCEPT, true); @@ -147,11 +129,9 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest { } } - private void runTask(Task task) throws CancelledException { - - task.run(TaskMonitor.DUMMY); - destinationProgram.flushEvents(); - waitForSwing(); + private void runTask(VtTask task) { + controller.runVTTask(task); + waitForProgram(destinationProgram); } private Data setData(DataType dataType, int dtLength, Address address, Program program) @@ -170,14 +150,4 @@ public class VTMatchAcceptTest extends AbstractGhidraHeadedIntegrationTest { } return data; } - - private class DomainObjectListenerRecorder implements DomainObjectListener { - - List events = new ArrayList(); - - @Override - public void domainObjectChanged(DomainObjectChangedEvent ev) { - events.add(ev); - } - } } diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java index 324d8f6ef4..aadd9288af 100644 --- a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java +++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchApplyTest.java @@ -69,7 +69,6 @@ public class VTMatchApplyTest extends AbstractGhidraHeadedIntegrationTest { private ProgramDB destinationProgram; private VTPlugin plugin; - // TODO: debug private DomainObjectListenerRecorder eventRecorder = new DomainObjectListenerRecorder(); @Before diff --git a/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchRemoveTest.java b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchRemoveTest.java new file mode 100644 index 0000000000..8d7e1c3078 --- /dev/null +++ b/Ghidra/Features/VersionTracking/src/test.slow/java/ghidra/feature/vt/api/VTMatchRemoveTest.java @@ -0,0 +1,354 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.feature.vt.api; + +import static ghidra.feature.vt.db.VTTestUtils.*; +import static org.junit.Assert.*; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.*; + +import docking.DialogComponentProvider; +import ghidra.feature.vt.api.db.VTSessionDB; +import ghidra.feature.vt.api.main.*; +import ghidra.feature.vt.db.DummyTestProgramCorrelator; +import ghidra.feature.vt.gui.plugin.*; +import ghidra.feature.vt.gui.task.*; +import ghidra.feature.vt.gui.util.VTOptionDefines; +import ghidra.framework.options.ToolOptions; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramDB; +import ghidra.program.model.address.Address; +import ghidra.program.model.data.*; +import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.*; +import ghidra.test.*; + +public class VTMatchRemoveTest extends AbstractGhidraHeadedIntegrationTest { + + private TestEnv env; + private PluginTool tool; + private VTController controller; + private VTPlugin plugin; + private VTSessionDB session; + private ProgramDB srcProgram; + private ProgramDB destProgram; + + @Before + public void setUp() throws Exception { + + env = new TestEnv(); + + ClassicSampleX86ProgramBuilder sourceBuilder = new ClassicSampleX86ProgramBuilder(); + srcProgram = sourceBuilder.getProgram(); + + ClassicSampleX86ProgramBuilder destinationBuilder = new ClassicSampleX86ProgramBuilder(); + destProgram = destinationBuilder.getProgram(); + + tool = env.getTool(); + tool.addPlugin(VTPlugin.class.getName()); + plugin = getPlugin(tool, VTPlugin.class); + controller = new VTControllerImpl(plugin); + + session = new VTSessionDB(testName.getMethodName() + " - Test Match Set Manager", + srcProgram, destProgram, this); + + runSwing(() -> controller.openVersionTrackingSession(session)); + } + + @After + public void tearDown() throws Exception { + env.dispose(); + } + + @Test + public void testRemoveMatch_UnaccpetedMatch() throws Exception { + + Address srcAddr = addr("0x0100808c", srcProgram); + Address destAddr = addr("0x0100808c", destProgram); + + setDataOnPrograms(srcAddr, destAddr); + String labelName = "Bob"; + addLabel(labelName, srcAddr, srcProgram); + + VTMatch match = createMatchSetWithOneDataMatch(session, srcAddr, destAddr); + + VTMatchSet matchSet = match.getMatchSet(); + remove(match, false); + assertMatchRemoved(matchSet, srcAddr, destAddr); + assertNoLabelApplied(labelName, destAddr); + } + + @Test + public void testRemoveMatch_AccpetedMatch() throws Exception { + + /* + Test: + - create and apply a match + - remove the match + - leave the applied markup after match removal + */ + + Address srcAddr = addr("0x0100808c", srcProgram); + Address destAddr = addr("0x0100808c", destProgram); + + setDataOnPrograms(srcAddr, destAddr); + String labelName = "Bob"; + addLabel(labelName, srcAddr, srcProgram); + + VTMatch match = createMatchSetWithOneDataMatch(session, srcAddr, destAddr); + setApplyDataLabelOnAccept(); + accept(match); + assertAcceptedAndLabelApplied(match, labelName, destAddr); + + VTMatchSet matchSet = match.getMatchSet(); + remove(match); + assertMatchRemoved(matchSet, srcAddr, destAddr); + assertLabelApplied(labelName, destAddr); + } + + @Test + public void testRemoveMatch_Accepted_MultipleMatchesForAssociation() throws Exception { + + /* + Test: + - create multiple matches for the same association + - apply one match + - remove the applied match + - leave the applied markup after match removal + + *This tests control flow that avoids execution when the match being removed is the last + match for an association. + + */ + + Address srcAddr = addr("0x0100808c", srcProgram); + Address destAddr = addr("0x0100808c", destProgram); + + setDataOnPrograms(srcAddr, destAddr); + String labelName = "Bob"; + addLabel(labelName, srcAddr, srcProgram); + + VTMatch match = createMatchSetWithMultipleMatchesToSameAssociation(srcAddr, destAddr); + setApplyDataLabelOnAccept(); + accept(match); + assertAcceptedAndLabelApplied(match, labelName, destAddr); + + VTMatchSet matchSet = match.getMatchSet(); + remove(match, false); + assertMatchRemoved(matchSet, srcAddr, destAddr); + assertLabelApplied(labelName, destAddr); + } + + @Test + public void testRemoveMatch_RejectedMatch() throws Exception { + + Address srcAddr = addr("0x0100808c", srcProgram); + Address destAddr = addr("0x0100808c", destProgram); + + setDataOnPrograms(srcAddr, destAddr); + String labelName = "Bob"; + addLabel(labelName, srcAddr, srcProgram); + + VTMatch match = createMatchSetWithOneDataMatch(session, srcAddr, destAddr); + setApplyDataLabelOnAccept(); + reject(match); + assertNoLabelApplied(labelName, destAddr); + + VTMatchSet matchSet = match.getMatchSet(); + remove(match, false); + assertMatchRemoved(matchSet, srcAddr, destAddr); + assertNoLabelApplied(labelName, destAddr); + } + + @Test + public void testRemoveMatch_AccpetedMatch_ChooseNotToDelete() throws Exception { + + /* + Test: + - create and apply a match + - remove the match, but cancel at dialog prompt + - match should still be valid; markup should still be applied + */ + + Address srcAddr = addr("0x0100808c", srcProgram); + Address destAddr = addr("0x0100808c", destProgram); + + setDataOnPrograms(srcAddr, destAddr); + String labelName = "Bob"; + addLabel(labelName, srcAddr, srcProgram); + + VTMatch match = createMatchSetWithOneDataMatch(session, srcAddr, destAddr); + setApplyDataLabelOnAccept(); + accept(match); + assertAcceptedAndLabelApplied(match, labelName, destAddr); + + VTMatchSet matchSet = match.getMatchSet(); + startRemoveThenCancel(match); + assertMatchNotRemoved(matchSet, srcAddr, destAddr); + assertLabelApplied(labelName, destAddr); + } + +//================================================================================================= +// Private Methods +//================================================================================================= + + private void remove(VTMatch match) { + remove(match, true); + } + + private void remove(VTMatch match, boolean expectPrompt) { + RemoveMatchTask task = new RemoveMatchTask(session, List.of(match)); + + AtomicBoolean finished = runTaskLater(task); // this task is blocking, so run later and wait + + if (expectPrompt) { + DialogComponentProvider removeDialog = + waitForDialogComponent("Delete ACCEPTED Matches?"); + pressButtonByText(removeDialog, "Delete Accepted Matches"); + } + + // let the task finish processing after pressing the button + waitFor(finished); + waitForProgram(destProgram); + } + + private void startRemoveThenCancel(VTMatch match) { + RemoveMatchTask task = new RemoveMatchTask(session, List.of(match)); + + AtomicBoolean finished = runTaskLater(task); // this task is blocking, so run later and wait + + DialogComponentProvider removeDialog = waitForDialogComponent("Delete ACCEPTED Matches?"); + pressButtonByText(removeDialog, "Finish"); + + // let the task finish processing after pressing the button + waitFor(finished); + waitForProgram(destProgram); + } + + private void assertLabelApplied(String labelName, Address addr) { + assertEquals(labelName, getSymbol(destProgram, addr).getName()); + } + + private void assertNoLabelApplied(String labelName, Address addr) { + Symbol symbol = getSymbol(destProgram, addr); + if (symbol == null) { + return; // no label; expected + } + assertNotEquals(labelName, symbol.getName()); + } + + private void assertMatchRemoved(VTMatchSet matchSet, Address srcAddr, Address destAddr) { + Collection matches = matchSet.getMatches(srcAddr, destAddr); + assertTrue(matches.isEmpty()); + } + + private void assertMatchNotRemoved(VTMatchSet matchSet, Address srcAddr, Address destAddr) { + Collection matches = matchSet.getMatches(srcAddr, destAddr); + assertFalse(matches.isEmpty()); + } + + private void assertAcceptedAndLabelApplied(VTMatch match, String labelName, Address addr) { + VTAssociationStatus status = match.getAssociation().getStatus(); + assertEquals(VTAssociationStatus.ACCEPTED, status); + assertEquals(labelName, getSymbol(destProgram, addr).getName()); + } + + private Symbol getSymbol(Program p, Address addr) { + return p.getSymbolTable().getPrimarySymbol(addr); + } + + private void setApplyDataLabelOnAccept() { + ToolOptions options = controller.getOptions(); + options.setBoolean(VTOptionDefines.APPLY_DATA_NAME_ON_ACCEPT, true); + } + + private void accept(VTMatch match) throws Exception { + AcceptMatchTask task = new AcceptMatchTask(controller, List.of(match)); + runTask(task); + } + + private void reject(VTMatch match) throws Exception { + RejectMatchTask task = new RejectMatchTask(session, List.of(match)); + runTask(task); + } + + private void setDataOnPrograms(Address srcAddr, Address destAddr) { + DataType srcDt = new DWordDataType(); + DataType destDt1 = new StringDataType(); + DataType destDt2 = new WordDataType(); + setData(srcDt, 4, srcAddr, srcProgram); + setData(destDt1, 2, destAddr, destProgram); + setData(destDt2, 2, destAddr.add(2), destProgram); + } + + private Symbol addLabel(String name, Address address, Program program) { + return tx(program, () -> { + SymbolTable symbolTable = program.getSymbolTable(); + return symbolTable.createLabel(address, name, SourceType.USER_DEFINED); + }); + } + + private Data setData(DataType dataType, int length, Address address, Program program) { + return tx(program, () -> { + Listing listing = program.getListing(); + return listing.createData(address, dataType, length); + }); + } + + private AtomicBoolean runTaskLater(VtTask task) { + AtomicBoolean finishedFlag = new AtomicBoolean(); + runSwingLater(() -> { + controller.runVTTask(task); + finishedFlag.set(true); + }); + waitForSwing(); + return finishedFlag; + } + + private void runTask(VtTask task) { + controller.runVTTask(task); + waitForProgram(destProgram); + } + + private VTMatch createMatchSetWithMultipleMatchesToSameAssociation(Address srcAddr, + Address destAddr) throws Exception { + int txId = 0; + try { + txId = session.startTransaction("Test Create Data Match Set"); + VTMatchInfo info = createRandomMatch(srcAddr, destAddr, session); + info.setAssociationType(VTAssociationType.DATA); + VTMatchSet matchSet = + session.createMatchSet(createProgramCorrelator(srcProgram, destProgram)); + VTMatch firstMatch = matchSet.addMatch(info); + + // create a second match, match set and correlator, all tied to the given association + DummyTestProgramCorrelator pc2 = + (DummyTestProgramCorrelator) createProgramCorrelator(srcProgram, destProgram); + pc2.setName("Correlator Two"); + VTMatchSet ms2 = session.createMatchSet(pc2); + ms2.addMatch(createRandomMatch(srcAddr, destAddr, session)); + + return firstMatch; + } + finally { + session.endTransaction(txId, true); + } + } +} diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/DummyTestProgramCorrelator.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/DummyTestProgramCorrelator.java index 195c1975f1..dc3d3ed2e4 100644 --- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/DummyTestProgramCorrelator.java +++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/DummyTestProgramCorrelator.java @@ -27,6 +27,7 @@ import ghidra.util.task.TaskMonitor; public class DummyTestProgramCorrelator extends VTAbstractProgramCorrelator { + private String name = "DummyTestProgramCorrelator"; private int matchCount = 1; public DummyTestProgramCorrelator() { @@ -84,8 +85,12 @@ public class DummyTestProgramCorrelator extends VTAbstractProgramCorrelator { } } + public void setName(String name) { + this.name = name; + } + @Override public String getName() { - return "DummyTestProgramCorrelator"; + return name; } } diff --git a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTDomainObjectEventsTest.java b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTDomainObjectEventsTest.java index 8bc8516901..fe2af6475c 100644 --- a/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTDomainObjectEventsTest.java +++ b/Ghidra/Features/VersionTracking/src/test/java/ghidra/feature/vt/db/VTDomainObjectEventsTest.java @@ -119,6 +119,7 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase { assertEquals(VTEvent.MATCH_ADDED, events.get(0).getEventType()); } + @SuppressWarnings("removal") // ignore the warning until removeMatch() is removed @Test public void testEventsForRemovingLastMatchForAssociation() { VTMatchSet manualMatchSet = db.getManualMatchSet(); @@ -134,6 +135,22 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase { assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType()); } + @Test + public void testEventsForDeletingLastMatchForAssociation() { + VTMatchSet manualMatchSet = db.getManualMatchSet(); + clearEvents(); + VTMatchInfo matchInfo = VTTestUtils.createRandomMatch(null); + VTMatch match = manualMatchSet.addMatch(matchInfo); + clearEvents(); + + manualMatchSet.deleteMatch(match); + + assertEventCount(2); + assertEquals(VTEvent.ASSOCIATION_REMOVED, events.get(0).getEventType()); + assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType()); + } + + @SuppressWarnings("removal") // ignore the warning until removeMatch() is removed @Test public void testEventsForRemovingNonLastMatchForAssociation() { VTMatchSet manualMatchSet = db.getManualMatchSet(); @@ -149,6 +166,21 @@ public class VTDomainObjectEventsTest extends VTBaseTestCase { assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType()); } + @Test + public void testEventsForDeletingNonLastMatchForAssociation() { + VTMatchSet manualMatchSet = db.getManualMatchSet(); + clearEvents(); + VTMatchInfo matchInfo = VTTestUtils.createRandomMatch(null); + VTMatch match = manualMatchSet.addMatch(matchInfo); + clearEvents(); + + manualMatchSet.deleteMatch(match); + + assertEventCount(2); + assertEquals(VTEvent.ASSOCIATION_REMOVED, events.get(0).getEventType()); + assertEquals(VTEvent.MATCH_DELETED, events.get(1).getEventType()); + } + @Test public void testEventsForRejectingMatch() throws VTAssociationStatusException { VTMatchSet matchSet = createMatchSet(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java index bec4d6aedd..e2f5e3bbed 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GTableFilterPanel.java @@ -24,29 +24,21 @@ import javax.swing.*; import javax.swing.border.BevelBorder; import javax.swing.event.*; import javax.swing.table.TableColumnModel; -import javax.swing.table.TableModel; import org.jdom.Element; import docking.DockingWindowManager; -import docking.menu.*; import docking.widgets.EmptyBorderButton; -import docking.widgets.EventTrigger; import docking.widgets.filter.*; import docking.widgets.label.GDLabel; import docking.widgets.table.columnfilter.ColumnBasedTableFilter; -import docking.widgets.table.columnfilter.ColumnFilterSaveManager; -import docking.widgets.table.constraint.dialog.ColumnFilterDialog; -import generic.theme.GIcon; +import docking.widgets.table.columnfilter.ColumnFilterManager; import ghidra.framework.options.PreferenceState; import ghidra.util.HelpLocation; import ghidra.util.Msg; -import ghidra.util.datastruct.WeakDataStructureFactory; -import ghidra.util.datastruct.WeakSet; import ghidra.util.exception.AssertException; import ghidra.util.task.SwingUpdateManager; import help.HelpService; -import resources.Icons; import utilities.util.reflection.ReflectionUtilities; import utility.function.Callback; @@ -112,37 +104,26 @@ public class GTableFilterPanel extends JPanel { public static final String FILTER_TEXTFIELD_NAME = "filter.panel.textfield"; private static final String FILTER_STATE = "FILTER_STATE"; private static final String FILTER_EXTENSION = ".FilterExtension"; - private static final Icon FILTER_ON_ICON = new GIcon("icon.widget.filterpanel.filter.on"); - private static final Icon FILTER_OFF_ICON = new GIcon("icon.widget.filterpanel.filter.off"); - private static final Icon APPLY_FILTER_ICON = Icons.OPEN_FOLDER_ICON; - private static final Icon CLEAR_FILTER_ICON = Icons.DELETE_ICON; private JTable table; - private RowObjectFilterModel textFilterModel; + private RowObjectFilterModel rowObjectFilterModel; private JLabel searchLabel; private FilterTextField filterField; private FilterListener filterListener = new GTableFilterListener(); - private WeakSet listeners = - WeakDataStructureFactory.createSingleThreadAccessWeakSet(); - private FilterOptions filterOptions = new FilterOptions(); private TableTextFilterFactory filterFactory = new DefaultTableTextFilterFactory<>(filterOptions); private RowFilterTransformer transformer; private TableFilter secondaryTableFilter; - private ColumnBasedTableFilter columnTableFilter; - private List> savedFilters = new ArrayList<>(); private EmptyBorderButton filterStateButton; + private ColumnFilterManager columnFilterManager; + private String uniquePreferenceKey; - private MultiStateDockingAction> columnFilterAction; - private ColumnFilterDialog columnFilterDialog; - private ColumnBasedTableFilter lastUsedColumnFilter; - - private SwingUpdateManager updateManager = new SwingUpdateManager(250, 1000, () -> { + private SwingUpdateManager filterUpdater = new SwingUpdateManager(250, 1000, () -> { String text = filterField.getText(); TableFilter tableFilter = filterFactory.getTableFilter(text, transformer); @@ -151,8 +132,9 @@ public class GTableFilterPanel extends JPanel { // result of a filter, the table does not know this and may update the wrong row data. table.editingCanceled(null); - textFilterModel.setTableFilter( - getCombinedTableFilter(secondaryTableFilter, tableFilter, columnTableFilter)); + ColumnBasedTableFilter columnFilter = columnFilterManager.getCurrentFilter(); + rowObjectFilterModel.setTableFilter( + getCombinedTableFilter(secondaryTableFilter, tableFilter, columnFilter)); }); /** I'm a field so that my weak reference won't go away */ @@ -174,12 +156,12 @@ public class GTableFilterPanel extends JPanel { @Override public void columnRemoved(TableColumnModelEvent e) { - updateTableContents(); + filterUpdater.updateLater(); } @Override public void columnAdded(TableColumnModelEvent e) { - updateTableContents(); + filterUpdater.updateLater(); } }; @@ -231,13 +213,16 @@ public class GTableFilterPanel extends JPanel { String filterLabel) { this.table = table; - buildPanel(filterLabel); - uniquePreferenceKey = createUniqueFilterPreferenceKey(table); transformer = new DefaultRowFilterTransformer<>(tableModel, table.getColumnModel()); - textFilterModel = installTableModel(tableModel); + rowObjectFilterModel = installTableModel(tableModel); + + columnFilterManager = new ColumnFilterManager(table, rowObjectFilterModel, + getPreferenceKey(), filterUpdater::updateLater); + + buildPanel(filterLabel); TableColumnModel columnModel = table.getColumnModel(); columnModel.addColumnModelListener(columnModelListener); @@ -246,12 +231,7 @@ public class GTableFilterPanel extends JPanel { table.addPropertyChangeListener(badProgrammingPropertyChangeListener); DockingWindowManager.registerComponentLoadedListener(this, - (windowManager, provider) -> initialize(windowManager)); - } - - private void initialize(DockingWindowManager windowManager) { - loadFilterPreference(windowManager); - initializeSavedFilters(); + (windowManager, provider) -> loadFilterPreference(windowManager)); } private void loadFilterPreference(DockingWindowManager dockingWindowManager) { @@ -283,7 +263,7 @@ public class GTableFilterPanel extends JPanel { if (xmlElement != null) { this.filterOptions = FilterOptions.restoreFromXML(xmlElement); updateFilterFactory(); - updateTableContents(); + filterUpdater.updateLater(); } } @@ -326,18 +306,7 @@ public class GTableFilterPanel extends JPanel { * @param newFilter the ColumnTableFilter to use for filtering this table. */ public void setColumnTableFilter(ColumnBasedTableFilter newFilter) { - if (Objects.equals(newFilter, this.columnTableFilter)) { - return; - } - if (columnTableFilter != null && !columnTableFilter.isSaved()) { - lastUsedColumnFilter = columnTableFilter; - } - columnTableFilter = newFilter; - updateTableContents(); - updateColumnFilterButton(); - if (columnFilterDialog != null) { - columnFilterDialog.filterChanged(newFilter); - } + columnFilterManager.setFilter(newFilter); } /** @@ -351,7 +320,7 @@ public class GTableFilterPanel extends JPanel { */ public void setFilterRowTransformer(RowFilterTransformer transformer) { this.transformer = transformer; - updateTableContents(); + filterUpdater.updateLater(); } /** @@ -362,7 +331,7 @@ public class GTableFilterPanel extends JPanel { */ public void setSecondaryFilter(TableFilter tableFilter) { this.secondaryTableFilter = tableFilter; - updateTableContents(); + filterUpdater.updateLater(); } /** @@ -373,7 +342,7 @@ public class GTableFilterPanel extends JPanel { public void setFilterOptions(FilterOptions filterOptions) { this.filterOptions = filterOptions; updateFilterFactory(); - updateTableContents(); + filterUpdater.updateLater(); doSaveState(); } @@ -394,7 +363,7 @@ public class GTableFilterPanel extends JPanel { add(buildFilterStateButton()); if (isTableColumnFilterableModel()) { add(Box.createHorizontalStrut(5)); - add(buildColumnFilterStateButton()); + add(columnFilterManager.getConfigureButton()); } HelpService helpService = DockingWindowManager.getHelpService(); @@ -425,107 +394,6 @@ public class GTableFilterPanel extends JPanel { return table.getModel() instanceof RowObjectFilterModel; } - @SuppressWarnings("unchecked") - private JComponent buildColumnFilterStateButton() { - - RowObjectFilterModel tableModel = - (RowObjectFilterModel) table.getModel(); - columnFilterAction = - new NonToolbarMultiStateAction<>("Column Filter", "GTableFilterPanel") { - - @Override - public void actionStateChanged( - ActionState> newActionState, - EventTrigger trigger) { - if (trigger != EventTrigger.GUI_ACTION) { - return; - } - ColumnFilterActionState state = (ColumnFilterActionState) newActionState; - state.performAction(); - } - - @Override - protected void actionPerformed() { - showFilterDialog(tableModel); - } - - }; - - HelpLocation helpLocation = new HelpLocation("Trees", "Column_Filters"); - columnFilterAction.setHelpLocation(helpLocation); - - updateFilterFactory(); - updateColumnFilterButton(); - JButton button = columnFilterAction.createButton(); - DockingWindowManager.getHelpService().registerHelp(button, helpLocation); - - return button; - } - - private void initializeSavedFilters() { - TableModel model = table.getModel(); - if (!(model instanceof GDynamicColumnTableModel)) { - return; - } - @SuppressWarnings("unchecked") - GDynamicColumnTableModel dynamicModel = - (GDynamicColumnTableModel) model; - - ColumnFilterSaveManager saveManager = - new ColumnFilterSaveManager<>(this, table, dynamicModel, dynamicModel.getDataSource()); - savedFilters = saveManager.getSavedFilters(); - Collections.reverse(savedFilters); - updateColumnFilterButton(); - } - - private void updateColumnFilterButton() { - List>> list = getActionStates(); - - columnFilterAction.setActionStates(list); - } - - private List>> getActionStates() { - List>> list = new ArrayList<>(); - if (columnTableFilter == null) { - list.add(new CreateFilterActionState()); - } - else { - list.add(new EditFilterActionState(columnTableFilter)); - list.add(new ClearFilterActionState()); - } - if (lastUsedColumnFilter != null) { - list.add(new ApplyLastUsedActionState(lastUsedColumnFilter)); - } - for (ColumnBasedTableFilter filter : savedFilters) { - list.add(new ApplyFilterActionState(filter)); - } - return list; - } - - private void showFilterDialog(RowObjectFilterModel tableModel) { - if (columnFilterDialog == null) { - if (ColumnFilterDialog.hasFilterableColumns(table, tableModel)) { - DockingWindowManager dockingWindowManager = DockingWindowManager.getInstance(table); - loadFilterPreference(dockingWindowManager); - columnFilterDialog = new ColumnFilterDialog<>(this, table, tableModel); - } - else { - Msg.showError(this, this, "Column Filter Error", - "This table contains no filterable columns!"); - return; - } - - } - - columnFilterDialog.setCloseCallback(() -> { - doSaveState(); - updateFilterFactory(); - columnFilterDialog = null; - }); - - DockingWindowManager.showDialog(GTableFilterPanel.this, columnFilterDialog); - } - private void updateFilterFactory() { filterStateButton.setIcon(filterOptions.getFilterStateIcon()); filterStateButton.setToolTipText(filterOptions.getFilterDescription()); @@ -587,17 +455,7 @@ public class GTableFilterPanel extends JPanel { } public RowObjectFilterModel getTableFilterModel() { - return textFilterModel; - } - - /** Convenience method to refilter the table's contents */ - private void updateTableContents() { - updateManager.updateLater(); - notifyFilterChanged(); - } - - private void notifyFilterChanged() { - listeners.forEach(callback -> callback.call()); + return rowObjectFilterModel; } public void dispose() { @@ -609,13 +467,11 @@ public class GTableFilterPanel extends JPanel { columnModel.removeColumnModelListener(columnModelListener); columnModelListener = null; - if (columnFilterDialog != null) { - columnFilterDialog.dispose(); - } + columnFilterManager.dispose(); table.removePropertyChangeListener(badProgrammingPropertyChangeListener); - updateManager.dispose(); + filterUpdater.dispose(); if (table instanceof GTable) { ((GTable) table).dispose(); } @@ -696,7 +552,7 @@ public class GTableFilterPanel extends JPanel { return viewRow; } - return textFilterModel.getModelRow(viewRow); + return rowObjectFilterModel.getModelRow(viewRow); } /** @@ -710,7 +566,7 @@ public class GTableFilterPanel extends JPanel { * @return the row in the table for the given model row. */ public int getViewRow(int modelRow) { - return textFilterModel.getViewRow(modelRow); + return rowObjectFilterModel.getViewRow(modelRow); } /** @@ -720,7 +576,7 @@ public class GTableFilterPanel extends JPanel { * @return the row object matching the given index */ public ROW_OBJECT getRowObject(int viewRow) { - ROW_OBJECT rowObject = textFilterModel.getRowObject(viewRow); + ROW_OBJECT rowObject = rowObjectFilterModel.getRowObject(viewRow); return rowObject; } @@ -736,7 +592,7 @@ public class GTableFilterPanel extends JPanel { return; } - int viewRow = textFilterModel.getViewIndex(t); + int viewRow = rowObjectFilterModel.getViewIndex(t); if (viewRow >= 0) { table.setRowSelectionInterval(viewRow, viewRow); scrollToSelectedRow(); @@ -785,7 +641,7 @@ public class GTableFilterPanel extends JPanel { if (row < 0) { return null; } - return textFilterModel.getRowObject(row); + return rowObjectFilterModel.getRowObject(row); } /** @@ -801,7 +657,7 @@ public class GTableFilterPanel extends JPanel { List list = new ArrayList<>(rows.length); for (int row : rows) { - list.add(textFilterModel.getRowObject(row)); + list.add(rowObjectFilterModel.getRowObject(row)); } return list; } @@ -814,7 +670,7 @@ public class GTableFilterPanel extends JPanel { * @return true if in the view */ public boolean isInView(ROW_OBJECT o) { - int rowIndex = textFilterModel.getRowIndex(o); + int rowIndex = rowObjectFilterModel.getRowIndex(o); return rowIndex >= 0; } @@ -823,11 +679,48 @@ public class GTableFilterPanel extends JPanel { } public int getRowCount() { - return textFilterModel.getRowCount(); + return rowObjectFilterModel.getRowCount(); } public int getUnfilteredRowCount() { - return textFilterModel.getUnfilteredRowCount(); + return rowObjectFilterModel.getUnfilteredRowCount(); + } + + /** + * Generates a key used to store user filter configuration state. You can override this + * method to generate unique keys yourself. You are required to override this method if + * you create multiple versions of a filter panel from the same place in your code, as + * multiple instances created in the same place will cause them all to share the same key and + * thus to have the same filter settings when they are created initially. + *

+ * As an example, consider a plugin that creates n providers. If each provider uses + * a filter panel, then each provider will share the same filter settings when that provider + * is created. If this is not what you want, then you need to override this method to + * generate a unique key for each provider. + * + * @param jTable the table + * @return a key used to store user filter configuration state. + */ + public String createUniqueFilterPreferenceKey(JTable jTable) { + return generateFilterPreferenceKey(jTable, FILTER_EXTENSION); + } + + /** + * Returns the ColumnTableFilter that has been set on this GTableFilterPanel or null if there + * is none. + * + * @return the ColumnTableFilter that has been set. + */ + public ColumnBasedTableFilter getColumnTableFilter() { + return columnFilterManager.getCurrentFilter(); + } + + /** + * Return a unique key that can be used to store preferences for this table. + * @return a unique key that can be used to store preferences for this table. + */ + public String getPreferenceKey() { + return uniquePreferenceKey; } //================================================================================================== @@ -900,7 +793,7 @@ public class GTableFilterPanel extends JPanel { } private TableModelEvent translateEventForFilter(TableModelEvent event) { - int rowCount = textFilterModel.getUnfilteredRowCount(); + int rowCount = rowObjectFilterModel.getUnfilteredRowCount(); if (rowCount == 0) { return event; // nothing to translate--no data } @@ -915,14 +808,14 @@ public class GTableFilterPanel extends JPanel { if (firstRow == 0 && lastRow == rowCount - 1) { firstRow = 0; - lastRow = Math.max(0, textFilterModel.getRowCount() - 1); + lastRow = Math.max(0, rowObjectFilterModel.getRowCount() - 1); } else { // translate to the filtered view (from the wrapped model's full universe) firstRow = getViewRow(firstRow); lastRow = getViewRow(lastRow); } - return new TableModelEvent(textFilterModel, firstRow, lastRow, event.getColumn(), + return new TableModelEvent(rowObjectFilterModel, firstRow, lastRow, event.getColumn(), event.getType()); } } @@ -937,8 +830,8 @@ public class GTableFilterPanel extends JPanel { } isUpdatingModel = true; - if (textFilterModel instanceof WrappingTableModel) { - WrappingTableModel tableModelWrapper = (WrappingTableModel) textFilterModel; + if (rowObjectFilterModel instanceof WrappingTableModel) { + WrappingTableModel tableModelWrapper = (WrappingTableModel) rowObjectFilterModel; tableModelWrapper.wrappedModelChangedFromTableChangedEvent(); } filterField.alert(); @@ -947,75 +840,16 @@ public class GTableFilterPanel extends JPanel { } private class GTableFilterListener implements FilterListener { - @Override public void filterChanged(String text) { - updateTableContents(); + filterUpdater.updateLater(); } } - /** - * Generates a key used to store user filter configuration state. You can override this - * method to generate unique keys yourself. You are required to override this method if - * you create multiple versions of a filter panel from the same place in your code, as - * multiple instances created in the same place will cause them all to share the same key and - * thus to have the same filter settings when they are created initially. - *

- * As an example, consider a plugin that creates n providers. If each provider uses - * a filter panel, then each provider will share the same filter settings when that provider - * is created. If this is not what you want, then you need to override this method to - * generate a unique key for each provider. - * - * @param jTable the table - * @return a key used to store user filter configuration state. - */ - public String createUniqueFilterPreferenceKey(JTable jTable) { - return generateFilterPreferenceKey(jTable, FILTER_EXTENSION); - } - - /** - * Returns the ColumnTableFilter that has been set on this GTableFilterPanel or null if there - * is none. - * - * @return the ColumnTableFilter that has been set. - */ - public ColumnBasedTableFilter getColumnTableFilter() { - return columnTableFilter; - } - - /** - * Return a unique key that can be used to store preferences for this table. - * @return a unique key that can be used to store preferences for this table. - */ - public String getPreferenceKey() { - return uniquePreferenceKey; - } - - /** - * Updates the "quick filter" multistate button. - * @param filter the filter to add or remove. - * @param add if true, the filter is added to the quick list. Otherwise, it is removed. - */ - public void updateSavedFilters(ColumnBasedTableFilter filter, boolean add) { - if (add) { - ArrayList> list = new ArrayList<>(); - list.add(filter); - list.addAll(savedFilters); - savedFilters = list; - if (filter.isEquivalent(columnTableFilter)) { - setColumnTableFilter(filter); - } - } - else { - savedFilters.remove(filter); - } - - updateColumnFilterButton(); - } - //================================================================================================== // Static Methods //================================================================================================== + private static String generateFilterPreferenceKey(JTable jTable, String extension) { if (jTable instanceof GTable) { @@ -1039,78 +873,4 @@ public class GTableFilterPanel extends JPanel { String clientName = filteredTrace[0].getClassName(); return clientName; } - -//================================================================================================== -// Inner Classes -//================================================================================================== - - private abstract class ColumnFilterActionState - extends ActionState> { - - ColumnFilterActionState(String name, Icon icon, ColumnBasedTableFilter filter) { - super(name, icon, filter); - } - - abstract void performAction(); - } - - String getFilterName(ColumnBasedTableFilter filter) { - String filterName = filter.getName(); - return filterName == null ? "Unsaved" : filterName; - } - - private class ClearFilterActionState extends ColumnFilterActionState { - public ClearFilterActionState() { - super("Clear Filter", CLEAR_FILTER_ICON, null); - } - - @Override - void performAction() { - setColumnTableFilter(null); - } - } - - private class CreateFilterActionState extends ColumnFilterActionState { - public CreateFilterActionState() { - super("Create Column Filter", FILTER_OFF_ICON, null); - } - - @Override - void performAction() { - showFilterDialog(textFilterModel); - } - } - - private class EditFilterActionState extends ColumnFilterActionState { - public EditFilterActionState(ColumnBasedTableFilter filter) { - super("Edit: " + getFilterName(filter), FILTER_ON_ICON, filter); - } - - @Override - void performAction() { - showFilterDialog(textFilterModel); - } - } - - private class ApplyFilterActionState extends ColumnFilterActionState { - public ApplyFilterActionState(ColumnBasedTableFilter filter) { - super("Apply: " + getFilterName(filter), APPLY_FILTER_ICON, filter); - } - - @Override - void performAction() { - setColumnTableFilter(getUserData()); - } - } - - private class ApplyLastUsedActionState extends ColumnFilterActionState { - public ApplyLastUsedActionState(ColumnBasedTableFilter filter) { - super("Apply Last Unsaved", FILTER_ON_ICON, filter); - } - - @Override - void performAction() { - setColumnTableFilter(getUserData()); - } - } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterManager.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterManager.java new file mode 100644 index 0000000000..0eeb3a67d8 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterManager.java @@ -0,0 +1,298 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docking.widgets.table.columnfilter; + +import java.util.*; + +import javax.swing.*; +import javax.swing.table.TableModel; + +import docking.DockingWindowManager; +import docking.menu.*; +import docking.widgets.EventTrigger; +import docking.widgets.table.GDynamicColumnTableModel; +import docking.widgets.table.RowObjectFilterModel; +import docking.widgets.table.constraint.dialog.ColumnFilterDialog; +import generic.theme.GIcon; +import ghidra.util.HelpLocation; +import ghidra.util.Msg; +import resources.Icons; +import utility.function.Callback; + +/** + * A class that manages column filters for a table. This includes creating the UI elements that + * allow users to build filters, as well as a means to save and restore filters. + * + * @param the row type + */ +public class ColumnFilterManager { + + public static final String FILTER_EXTENSION = ".FilterExtension"; + public static final String FILTER_TEXTFIELD_NAME = "filter.panel.textfield"; + private static final Icon FILTER_ON_ICON = new GIcon("icon.widget.filterpanel.filter.on"); + private static final Icon FILTER_OFF_ICON = new GIcon("icon.widget.filterpanel.filter.off"); + private static final Icon APPLY_FILTER_ICON = Icons.OPEN_FOLDER_ICON; + private static final Icon CLEAR_FILTER_ICON = Icons.DELETE_ICON; + + private MultiStateDockingAction> columnFilterAction; + private JButton configureButton; + private ColumnFilterDialog columnFilterDialog; + + private ColumnBasedTableFilter lastUsedFilter; + private ColumnBasedTableFilter currentFilter; + private List> savedFilters = new ArrayList<>(); + + private JTable table; + private RowObjectFilterModel rowObjectFilterModel; + private String preferenceKey; + private Callback filterChangedCallback; + + public ColumnFilterManager(JTable table, RowObjectFilterModel rowObjectFilterModel, + String preferenceKey, Callback filterChangedCallback) { + this.table = Objects.requireNonNull(table); + this.rowObjectFilterModel = Objects.requireNonNull(rowObjectFilterModel); + this.preferenceKey = Objects.requireNonNull(preferenceKey); + this.filterChangedCallback = Objects.requireNonNull(filterChangedCallback); + + configureButton = buildColumnFilterStateButton(); + + DockingWindowManager.registerComponentLoadedListener(table, + (windowManager, provider) -> initializeSavedFilters()); + } + + private void initializeSavedFilters() { + TableModel model = table.getModel(); + if (!(model instanceof GDynamicColumnTableModel)) { + return; + } + + @SuppressWarnings("unchecked") + GDynamicColumnTableModel dynamicModel = + (GDynamicColumnTableModel) model; + + ColumnFilterSaveManager saveManager = new ColumnFilterSaveManager<>( + preferenceKey, table, dynamicModel, dynamicModel.getDataSource()); + + savedFilters = saveManager.getSavedFilters(); + Collections.reverse(savedFilters); + updateColumnFilterButton(); + } + + public ColumnBasedTableFilter getCurrentFilter() { + return currentFilter; + } + + public JButton getConfigureButton() { + return configureButton; + } + + public String getPreferenceKey() { + return preferenceKey; + } + + public void setFilter(ColumnBasedTableFilter newFilter) { + if (Objects.equals(newFilter, this.currentFilter)) { + return; + } + + if (currentFilter != null && !currentFilter.isSaved()) { + lastUsedFilter = currentFilter; + } + currentFilter = newFilter; + + updateColumnFilterButton(); + if (columnFilterDialog != null) { + columnFilterDialog.filterChanged(newFilter); + } + + filterChangedCallback.call(); + } + + public void updateSavedFilters(ColumnBasedTableFilter filter, boolean add) { + + if (add) { + ArrayList> list = new ArrayList<>(); + list.add(filter); + list.addAll(savedFilters); + savedFilters = list; + if (filter.isEquivalent(currentFilter)) { + setFilter(filter); + } + } + else { + savedFilters.remove(filter); + } + + updateColumnFilterButton(); + + filterChangedCallback.call(); + } + + public void dispose() { + if (columnFilterDialog != null) { + columnFilterDialog.dispose(); + columnFilterDialog = null; + } + + filterChangedCallback = Callback.dummy(); + } + + private JButton buildColumnFilterStateButton() { + + columnFilterAction = + new NonToolbarMultiStateAction<>("Column Filter", "GTableFilterPanel") { + + @Override + public void actionStateChanged( + ActionState> newActionState, + EventTrigger trigger) { + if (trigger != EventTrigger.GUI_ACTION) { + return; + } + ColumnFilterActionState state = (ColumnFilterActionState) newActionState; + state.performAction(); + } + + @Override + protected void actionPerformed() { + showFilterDialog(rowObjectFilterModel); + } + + }; + + HelpLocation helpLocation = new HelpLocation("Trees", "Column_Filters"); + columnFilterAction.setHelpLocation(helpLocation); + + updateColumnFilterButton(); + JButton button = columnFilterAction.createButton(); + DockingWindowManager.getHelpService().registerHelp(button, helpLocation); + + return button; + } + + private void updateColumnFilterButton() { + List>> list = getActionStates(); + columnFilterAction.setActionStates(list); + } + + private List>> getActionStates() { + List>> list = new ArrayList<>(); + if (currentFilter == null) { + list.add(new CreateFilterActionState()); + } + else { + list.add(new EditFilterActionState(currentFilter)); + list.add(new ClearFilterActionState()); + } + if (lastUsedFilter != null) { + list.add(new ApplyLastUsedActionState(lastUsedFilter)); + } + for (ColumnBasedTableFilter filter : savedFilters) { + list.add(new ApplyFilterActionState(filter)); + } + return list; + } + + private void showFilterDialog(RowObjectFilterModel tableModel) { + if (columnFilterDialog == null) { + if (ColumnFilterDialog.hasFilterableColumns(table, tableModel)) { + columnFilterDialog = new ColumnFilterDialog<>(this, table, rowObjectFilterModel); + } + else { + Msg.showError(this, null, "Column Filter Error", + "This table contains no filterable columns!"); + return; + } + + } + + DockingWindowManager.showDialog(table, columnFilterDialog); + } + +//================================================================================================== +// Inner Classes +//================================================================================================== + + private abstract class ColumnFilterActionState + extends ActionState> { + + ColumnFilterActionState(String name, Icon icon, ColumnBasedTableFilter filter) { + super(name, icon, filter); + } + + abstract void performAction(); + } + + String getFilterName(ColumnBasedTableFilter filter) { + String filterName = filter.getName(); + return filterName == null ? "Unsaved" : filterName; + } + + private class ClearFilterActionState extends ColumnFilterActionState { + public ClearFilterActionState() { + super("Clear Filter", CLEAR_FILTER_ICON, null); + } + + @Override + void performAction() { + setFilter(null); + } + } + + private class CreateFilterActionState extends ColumnFilterActionState { + public CreateFilterActionState() { + super("Create Column Filter", FILTER_OFF_ICON, null); + } + + @Override + void performAction() { + showFilterDialog(rowObjectFilterModel); + } + } + + private class EditFilterActionState extends ColumnFilterActionState { + public EditFilterActionState(ColumnBasedTableFilter filter) { + super("Edit: " + getFilterName(filter), FILTER_ON_ICON, filter); + } + + @Override + void performAction() { + showFilterDialog(rowObjectFilterModel); + } + } + + private class ApplyFilterActionState extends ColumnFilterActionState { + public ApplyFilterActionState(ColumnBasedTableFilter filter) { + super("Apply: " + getFilterName(filter), APPLY_FILTER_ICON, filter); + } + + @Override + void performAction() { + setFilter(getUserData()); + } + } + + private class ApplyLastUsedActionState extends ColumnFilterActionState { + public ApplyLastUsedActionState(ColumnBasedTableFilter filter) { + super("Apply Last Unsaved", FILTER_ON_ICON, filter); + } + + @Override + void performAction() { + setFilter(getUserData()); + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterSaveManager.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterSaveManager.java index 12c09f1faf..bfa795d0e8 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterSaveManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/columnfilter/ColumnFilterSaveManager.java @@ -23,7 +23,6 @@ import javax.swing.JTable; import org.jdom.Element; import docking.DockingWindowManager; -import docking.widgets.table.GTableFilterPanel; import docking.widgets.table.RowObjectTableModel; import ghidra.framework.options.PreferenceState; import ghidra.framework.options.SaveState; @@ -35,7 +34,7 @@ import ghidra.util.Msg; * @param the row type of the table. */ public class ColumnFilterSaveManager { - private static final String COLUMN_FILTER_EXTENSION = ".ColumnFilterExtension"; + private static final String COLUMN_FILTER_STATE = "COLUMN_FILTER_STATE"; private List> filters = new ArrayList<>(); @@ -46,14 +45,15 @@ public class ColumnFilterSaveManager { /** * Constructor * - * @param panel The GTableFilterPanel for the table. + * @param tablePreferenceKey the key used to save table settings. This is used to make a + * preference key for saving the column filters. * @param table The JTable that is filterable. * @param model the TableModel that supports filtering. * @param dataSource the table's DataSource object. */ - public ColumnFilterSaveManager(GTableFilterPanel panel, JTable table, + public ColumnFilterSaveManager(String tablePreferenceKey, JTable table, RowObjectTableModel model, Object dataSource) { - preferenceKey = panel.getPreferenceKey() + COLUMN_FILTER_EXTENSION; + preferenceKey = tablePreferenceKey + ColumnFilterManager.FILTER_EXTENSION; loadFromPreferences(table, model, dataSource); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialog.java index ee036b3f00..9c94cd40d3 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constraint/dialog/ColumnFilterDialog.java @@ -29,7 +29,6 @@ import docking.action.*; import docking.widgets.OptionDialog; import docking.widgets.dialogs.InputDialog; import docking.widgets.label.GLabel; -import docking.widgets.table.GTableFilterPanel; import docking.widgets.table.RowObjectFilterModel; import docking.widgets.table.columnfilter.*; import docking.widgets.table.constrainteditor.ColumnConstraintEditor; @@ -50,38 +49,38 @@ import utility.function.Callback; public class ColumnFilterDialog extends ReusableDialogComponentProvider implements TableFilterDialogModelListener { - private final ColumnFilterDialogModel filterModel; + private ColumnFilterManager filterManager; + private ColumnFilterDialogModel dialogModel; + + private JTable table; + private RowObjectFilterModel tableModel; private JPanel filterPanelContainer; private List filterPanels = new ArrayList<>(); private Callback closeCallback; - private GTableFilterPanel gTableFilterPanel; private JPanel bottomPanel; - private JTable table; - private RowObjectFilterModel tableModel; - /** * Constructor - * - * @param gTableFilterPanel the GTableFilterPanel that launched this dialog. + * + * @param filterManager the filter manager * @param table the table being filtered. * @param tableModel the table model. */ - public ColumnFilterDialog(GTableFilterPanel gTableFilterPanel, JTable table, + public ColumnFilterDialog(ColumnFilterManager filterManager, JTable table, RowObjectFilterModel tableModel) { super("Table Column Filters", WindowUtilities.areModalDialogsVisible(), true, true, false); - this.gTableFilterPanel = gTableFilterPanel; + this.filterManager = filterManager; this.table = table; this.tableModel = tableModel; - ColumnBasedTableFilter columnTableFilter = gTableFilterPanel.getColumnTableFilter(); + ColumnBasedTableFilter columnTableFilter = filterManager.getCurrentFilter(); - filterModel = + dialogModel = new ColumnFilterDialogModel<>(tableModel, table.getColumnModel(), columnTableFilter); - filterModel.addListener(this); + dialogModel.addListener(this); setHelpLocation(new HelpLocation("Trees", "Column_Filters")); addWorkPanel(buildMainPanel()); @@ -97,10 +96,9 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider updateStatus(); } - public static boolean hasFilterableColumns(JTable table, - RowObjectFilterModel model) { + public static boolean hasFilterableColumns(JTable table, RowObjectFilterModel model) { return !ColumnFilterDialogModel.getAllColumnFilterData(model, table.getColumnModel()) - .isEmpty(); + .isEmpty(); } private void addClearFilterButton() { @@ -119,7 +117,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider DockingAction saveAction = new DockingAction("Save", "Filter") { @Override public boolean isEnabledForContext(ActionContext context) { - return !filterModel.getFilterRows().isEmpty() && filterModel.isValid(); + return !dialogModel.getFilterRows().isEmpty() && dialogModel.isValid(); } @Override @@ -140,15 +138,15 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider }; loadAction.setDescription("Load Filter"); loadAction.setHelpLocation(new HelpLocation("Trees", "Load_Filter")); - loadAction.setToolBarData( - new ToolBarData(Icons.OPEN_FOLDER_ICON)); + loadAction.setToolBarData(new ToolBarData(Icons.OPEN_FOLDER_ICON)); addAction(loadAction); } private void saveFilter() { - ColumnFilterSaveManager filterSaveManager = new ColumnFilterSaveManager<>( - gTableFilterPanel, table, tableModel, filterModel.getDataSource()); - ColumnBasedTableFilter filter = filterModel.getTableColumnFilter(); + String preferenceKey = filterManager.getPreferenceKey(); + ColumnFilterSaveManager filterSaveManager = new ColumnFilterSaveManager<>(preferenceKey, + table, tableModel, dialogModel.getDataSource()); + ColumnBasedTableFilter filter = dialogModel.getTableColumnFilter(); String defaultName = new Date().toString(); InputDialog dialog = new InputDialog("Save Filter", "Filter Name: ", defaultName, d -> { @@ -174,13 +172,14 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider filter.setName(filterName); filterSaveManager.addFilter(filter); filterSaveManager.save(); - gTableFilterPanel.updateSavedFilters(filter, true); - filterModel.setFilter(filter); + filterManager.updateSavedFilters(filter, true); + dialogModel.setFilter(filter); } private void loadFilter() { - ColumnFilterSaveManager filterSaveManager = new ColumnFilterSaveManager<>( - gTableFilterPanel, table, tableModel, filterModel.getDataSource()); + String preferenceKey = filterManager.getPreferenceKey(); + ColumnFilterSaveManager filterSaveManager = new ColumnFilterSaveManager<>(preferenceKey, + table, tableModel, dialogModel.getDataSource()); List> savedFilters = filterSaveManager.getSavedFilters(); if (savedFilters.isEmpty()) { Msg.showInfo(this, getComponent(), "No Saved Filters", @@ -195,7 +194,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider ColumnBasedTableFilter selectedFilter = archiveDialog.getSelectedColumnFilter(); if (selectedFilter != null) { - filterModel.setFilter(selectedFilter); + dialogModel.setFilter(selectedFilter); } } @@ -223,14 +222,12 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider bottomPanel = new JPanel(new BorderLayout()); JPanel innerPanel = new JPanel(new VerticalLayout(3)); - JButton addAndConditionButton = - new JButton("Add AND condition", Icons.ADD_ICON); + JButton addAndConditionButton = new JButton("Add AND condition", Icons.ADD_ICON); addAndConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.AND)); addAndConditionButton.setEnabled(true); - JButton addOrConditionButton = - new JButton("Add OR condition", Icons.ADD_ICON); + JButton addOrConditionButton = new JButton("Add OR condition", Icons.ADD_ICON); addOrConditionButton.setHorizontalAlignment(SwingConstants.LEFT); addOrConditionButton.addActionListener(e -> addFilterCondition(LogicOperation.OR)); @@ -251,7 +248,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider } sb.append("Column Filter"); - ColumnBasedTableFilter filter = filterModel.getTableColumnFilter(); + ColumnBasedTableFilter filter = dialogModel.getTableColumnFilter(); if (filter != null && filter.getName() != null) { sb.append(": ").append(filter.getName()); } @@ -275,7 +272,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider // * Dialog state is different from applied filter and valid - prompt to apply filter. // * Dialog state is different from applied filter, but invalid - prompt if should really close - if (!filterModel.hasUnappliedChanges()) { + if (!dialogModel.hasUnappliedChanges()) { return true; } if (dialogHasValidFilter()) { @@ -316,12 +313,12 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider } private boolean dialogHasValidFilter() { - return filterModel.getTableColumnFilter() != null; + return dialogModel.getTableColumnFilter() != null; } @Override protected void dialogClosed() { - filterModel.dispose(); + dialogModel.dispose(); if (closeCallback != null) { closeCallback.call(); } @@ -339,28 +336,29 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider } private void clearFilter() { - this.gTableFilterPanel.setColumnTableFilter(null); - filterModel.clear(); + filterManager.setFilter(null); + dialogModel.clear(); updateStatus(); } private void applyFilter() { - ColumnBasedTableFilter tableColumnFilter = filterModel.getTableColumnFilter(); - filterModel.setCurrentlyAppliedFilter(tableColumnFilter); - this.gTableFilterPanel.setColumnTableFilter(tableColumnFilter); + ColumnBasedTableFilter tableColumnFilter = dialogModel.getTableColumnFilter(); + dialogModel.setCurrentlyAppliedFilter(tableColumnFilter); + filterManager.setFilter(tableColumnFilter); } private void loadFilterRows() { filterPanelContainer.removeAll(); filterPanels.clear(); - List filterRows = filterModel.getFilterRows(); + List filterRows = dialogModel.getFilterRows(); for (int i = 0; i < filterRows.size(); i++) { DialogFilterRow filterRow = filterRows.get(i); ColumnFilterPanel panel = new ColumnFilterPanel(filterRow); if (i != 0) { - filterPanelContainer.add( - createLogicalOperationLabel(filterRow.getLogicOperation())); + LogicOperation op = filterRow.getLogicOperation(); + GLabel label = createLogicalOperationLabel(op); + filterPanelContainer.add(label); } filterPanelContainer.add(panel); filterPanels.add(panel); @@ -382,14 +380,14 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider headerPanel.add(new GLabel("Filter", SwingConstants.CENTER)); headerPanel.add(new GLabel("Filter Value", SwingConstants.CENTER)); - headerPanel.setBorder(new CompoundBorder( - BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.BORDER), - BorderFactory.createEmptyBorder(4, 0, 4, 0))); + headerPanel.setBorder( + new CompoundBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Colors.BORDER), + BorderFactory.createEmptyBorder(4, 0, 4, 0))); return headerPanel; } private void addFilterCondition(LogicOperation logicalOperation) { - filterModel.createFilterRow(logicalOperation); + dialogModel.createFilterRow(logicalOperation); scrollFilterPanelToBottom(); } @@ -410,7 +408,7 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider void updateStatus() { setStatusText(getStatusMessage()); - boolean isValid = filterModel.isValid(); + boolean isValid = dialogModel.isValid(); setOkEnabled(isValid); setApplyEnabled(isValid); @@ -423,11 +421,11 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider } public void filterChanged(ColumnBasedTableFilter newFilter) { - if (Objects.equals(newFilter, filterModel.getTableColumnFilter())) { + if (Objects.equals(newFilter, dialogModel.getTableColumnFilter())) { return; } getComponent().requestFocus(); // work around for java parenting bug where dialog appears behind - if (filterModel.hasUnappliedChanges()) { + if (dialogModel.hasUnappliedChanges()) { int result = OptionDialog.showYesNoDialog(getComponent(), "Filter Changed", "The filter has been changed externally.\n" + " Do you want to update this editor and lose your current changes?"); @@ -435,14 +433,14 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider return; } } - filterModel.setFilter(newFilter); + dialogModel.setFilter(newFilter); } private String getStatusMessage() { - if (filterModel.isEmpty()) { + if (dialogModel.isEmpty()) { return "Please add a filter condition!"; } - if (!filterModel.isValid()) { + if (!dialogModel.isValid()) { return "One or more filter values are invalid!"; } return ""; @@ -473,8 +471,6 @@ public class ColumnFilterDialog extends ReusableDialogComponentProvider } void filterRemoved(ColumnBasedTableFilter filter) { - gTableFilterPanel.updateSavedFilters(filter, false); - + filterManager.updateSavedFilters(filter, false); } - } diff --git a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java index 61a1e8cd19..e41233be05 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/ConcurrentQBuilder.java @@ -193,6 +193,12 @@ public class ConcurrentQBuilder { return this; } + /** + * Builds the final {@link ConcurrentQ}. + * + * @param callback the callback for processing each job + * @return the new queue + */ public ConcurrentQ build(QCallback callback) { ConcurrentQ concurrentQ = new ConcurrentQ<>(callback, getQueue(), getThreadPool(), diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/util/Lock.java b/Ghidra/Framework/Project/src/main/java/ghidra/util/Lock.java index 7a42b51b82..8b799f4e09 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/util/Lock.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/util/Lock.java @@ -17,7 +17,7 @@ package ghidra.util; /** * Ghidra synchronization lock. This class allows creation of named locks for - * synchroniing modification of multiple tables in the Ghidra database. + * synchronizing modification of multiple tables in the Ghidra database. */ public class Lock { private Thread owner;