gui: add full text search (#74)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package jadx.gui.treemodel;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.gui.utils.Utils;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
public class CodeNode extends JClass {
|
||||
|
||||
private static final ImageIcon ICON = Utils.openIcon("file_obj");
|
||||
|
||||
private final String line;
|
||||
private final int lineNum;
|
||||
|
||||
public CodeNode(JavaClass javaClass, int lineNum, String line) {
|
||||
super(javaClass, (JClass) makeFrom(javaClass.getDeclaringClass()));
|
||||
this.line = line;
|
||||
this.lineNum = lineNum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Icon getIcon() {
|
||||
return ICON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLine() {
|
||||
return lineNum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String makeString() {
|
||||
return getCls().getFullName() + ":" + lineNum + " " + line;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String makeLongString() {
|
||||
return makeString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return makeString();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import jadx.gui.treemodel.JRoot;
|
||||
import jadx.gui.update.JadxUpdate;
|
||||
import jadx.gui.update.JadxUpdate.IUpdateCallback;
|
||||
import jadx.gui.update.data.Release;
|
||||
import jadx.gui.utils.CacheObject;
|
||||
import jadx.gui.utils.Link;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.Position;
|
||||
@@ -88,6 +89,7 @@ public class MainWindow extends JFrame {
|
||||
|
||||
private final JadxWrapper wrapper;
|
||||
private final JadxSettings settings;
|
||||
private final CacheObject cacheObject;
|
||||
|
||||
private JPanel mainPanel;
|
||||
|
||||
@@ -105,6 +107,7 @@ public class MainWindow extends JFrame {
|
||||
public MainWindow(JadxSettings settings) {
|
||||
this.wrapper = new JadxWrapper(settings);
|
||||
this.settings = settings;
|
||||
this.cacheObject = new CacheObject();
|
||||
|
||||
initUI();
|
||||
initMenuAndToolbar();
|
||||
@@ -162,6 +165,7 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
public void openFile(File file) {
|
||||
cacheObject.reset();
|
||||
wrapper.openFile(file);
|
||||
deobfToggleBtn.setSelected(settings.isDeobfuscationOn());
|
||||
settings.addRecentFile(file.getAbsolutePath());
|
||||
@@ -355,10 +359,9 @@ public class MainWindow extends JFrame {
|
||||
nav.add(search);
|
||||
ActionListener searchAction = new ActionListener() {
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
final SearchDialog dialog = new SearchDialog(MainWindow.this, tabbedPane, wrapper);
|
||||
dialog.prepare();
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
SearchDialog dialog = new SearchDialog(MainWindow.this, tabbedPane, wrapper);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
});
|
||||
@@ -606,6 +609,10 @@ public class MainWindow extends JFrame {
|
||||
return settings;
|
||||
}
|
||||
|
||||
public CacheObject getCacheObject() {
|
||||
return cacheObject;
|
||||
}
|
||||
|
||||
private class OpenListener implements ActionListener {
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
openFile();
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package jadx.gui.ui;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.api.JavaField;
|
||||
import jadx.api.JavaMethod;
|
||||
import jadx.api.JavaNode;
|
||||
import jadx.gui.JadxWrapper;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.treemodel.TextNode;
|
||||
import jadx.gui.utils.CacheObject;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.NameIndex;
|
||||
import jadx.gui.utils.Position;
|
||||
import jadx.gui.utils.TextSearchIndex;
|
||||
import jadx.gui.utils.TextStandardActions;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
@@ -39,72 +38,114 @@ import java.awt.Container;
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Frame;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.ItemEvent;
|
||||
import java.awt.event.ItemListener;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.Collections;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SearchDialog extends JDialog {
|
||||
|
||||
private static final long serialVersionUID = -5105405456969134105L;
|
||||
|
||||
private static final int MAX_RESULTS_COUNT = 100;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SearchDialog.class);
|
||||
private static final int MAX_RESULTS_COUNT = 500;
|
||||
|
||||
private static enum SearchOptions {
|
||||
private enum SearchOptions {
|
||||
CLASS,
|
||||
METHOD,
|
||||
FIELD,
|
||||
CODE
|
||||
}
|
||||
|
||||
private static final Set<SearchOptions> OPTIONS =
|
||||
EnumSet.of(SearchOptions.CLASS, SearchOptions.METHOD, SearchOptions.FIELD);
|
||||
private static final Set<SearchOptions> OPTIONS = EnumSet.allOf(SearchOptions.class);
|
||||
|
||||
private final TabbedPane tabbedPane;
|
||||
private final JadxWrapper wrapper;
|
||||
private NameIndex<JavaNode> index;
|
||||
private final CacheObject cache;
|
||||
|
||||
private JTextField searchField;
|
||||
private ResultsModel resultsModel;
|
||||
private JList resultsList;
|
||||
private JProgressBar busyBar;
|
||||
|
||||
public SearchDialog(Frame owner, TabbedPane tabbedPane, JadxWrapper wrapper) {
|
||||
super(owner);
|
||||
public SearchDialog(MainWindow mainWindow, TabbedPane tabbedPane, JadxWrapper wrapper) {
|
||||
super(mainWindow);
|
||||
this.tabbedPane = tabbedPane;
|
||||
this.wrapper = wrapper;
|
||||
this.cache = mainWindow.getCacheObject();
|
||||
|
||||
initUI();
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowActivated(WindowEvent e) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
prepare();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void prepare() {
|
||||
TextSearchIndex index = cache.getTextIndex();
|
||||
if (index != null) {
|
||||
return;
|
||||
}
|
||||
LoadTask task = new LoadTask();
|
||||
task.init();
|
||||
task.execute();
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
index = new NameIndex<JavaNode>();
|
||||
for (JavaClass cls : wrapper.getClasses()) {
|
||||
indexClass(cls);
|
||||
TextSearchIndex index = cache.getTextIndex();
|
||||
if (index != null) {
|
||||
return;
|
||||
}
|
||||
index = new TextSearchIndex();
|
||||
for (JavaClass cls : wrapper.getClasses()) {
|
||||
index.indexNames(cls);
|
||||
}
|
||||
for (JavaClass cls : wrapper.getClasses()) {
|
||||
index.indexCode(cls);
|
||||
}
|
||||
cache.setTextIndex(index);
|
||||
}
|
||||
|
||||
private synchronized void performSearch() {
|
||||
resultsModel.removeAllElements();
|
||||
String text = searchField.getText();
|
||||
List<JavaNode> results;
|
||||
if (text == null || text.isEmpty() || index == null) {
|
||||
results = Collections.emptyList();
|
||||
} else {
|
||||
results = index.search(text);
|
||||
if (text == null || text.isEmpty() || OPTIONS.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
resultsModel.setResults(results);
|
||||
TextSearchIndex index = cache.getTextIndex();
|
||||
if (index == null) {
|
||||
return;
|
||||
}
|
||||
if (OPTIONS.contains(SearchOptions.CLASS)) {
|
||||
resultsModel.addAll(index.searchClsName(text));
|
||||
}
|
||||
if (OPTIONS.contains(SearchOptions.METHOD)) {
|
||||
resultsModel.addAll(index.searchMthName(text));
|
||||
}
|
||||
if (OPTIONS.contains(SearchOptions.FIELD)) {
|
||||
resultsModel.addAll(index.searchFldName(text));
|
||||
}
|
||||
if (OPTIONS.contains(SearchOptions.CODE)) {
|
||||
resultsModel.addAll(index.searchCode(text));
|
||||
}
|
||||
LOG.info("Search returned {} results", resultsModel.size());
|
||||
}
|
||||
|
||||
private void openSelectedItem() {
|
||||
@@ -118,39 +159,12 @@ public class SearchDialog extends JDialog {
|
||||
dispose();
|
||||
}
|
||||
|
||||
private void indexClass(JavaClass cls) {
|
||||
if (OPTIONS.contains(SearchOptions.CLASS)) {
|
||||
index.add(cls.getFullName(), cls);
|
||||
}
|
||||
if (OPTIONS.contains(SearchOptions.METHOD)) {
|
||||
for (JavaMethod mth : cls.getMethods()) {
|
||||
index.add(mth.getFullName(), mth);
|
||||
}
|
||||
}
|
||||
if (OPTIONS.contains(SearchOptions.FIELD)) {
|
||||
for (JavaField fld : cls.getFields()) {
|
||||
index.add(fld.getFullName(), fld);
|
||||
}
|
||||
}
|
||||
if (OPTIONS.contains(SearchOptions.CODE)) {
|
||||
String code = cls.getCode();
|
||||
index.add(code, cls);
|
||||
}
|
||||
for (JavaClass innerCls : cls.getInnerClasses()) {
|
||||
indexClass(innerCls);
|
||||
}
|
||||
}
|
||||
|
||||
private class LoadTask extends SwingWorker<Void, Void> {
|
||||
public void init() {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
busyBar.setVisible(true);
|
||||
searchField.setEnabled(false);
|
||||
resultsList.setEnabled(false);
|
||||
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
|
||||
}
|
||||
});
|
||||
public LoadTask() {
|
||||
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
|
||||
busyBar.setVisible(true);
|
||||
searchField.setEnabled(false);
|
||||
resultsList.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -175,14 +189,15 @@ public class SearchDialog extends JDialog {
|
||||
private static class ResultsModel extends DefaultListModel {
|
||||
private static final long serialVersionUID = -7821286846923903208L;
|
||||
|
||||
private void setResults(List<JavaNode> results) {
|
||||
removeAllElements();
|
||||
if (results.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int count = Math.min(results.size(), MAX_RESULTS_COUNT);
|
||||
for (int i = 0; i < count; i++) {
|
||||
addElement(JNode.makeFrom(results.get(i)));
|
||||
private void addAll(Iterable<? extends JNode> nodes) {
|
||||
for (JNode node : nodes) {
|
||||
if (size() >= MAX_RESULTS_COUNT) {
|
||||
if (size() == MAX_RESULTS_COUNT) {
|
||||
addElement(new TextNode("Search results truncated (limit: " + MAX_RESULTS_COUNT + ")"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
addElement(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,7 +258,6 @@ public class SearchDialog extends JDialog {
|
||||
JCheckBox mthChBox = makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD);
|
||||
JCheckBox fldChBox = makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD);
|
||||
JCheckBox codeChBox = makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE);
|
||||
codeChBox.setEnabled(false);
|
||||
|
||||
resultsModel = new ResultsModel();
|
||||
resultsList = new JList(resultsModel);
|
||||
@@ -307,12 +321,24 @@ public class SearchDialog extends JDialog {
|
||||
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
|
||||
buttonPane.add(cancelButton);
|
||||
|
||||
Container contentPane = getContentPane();
|
||||
final Container contentPane = getContentPane();
|
||||
contentPane.add(searchPane, BorderLayout.PAGE_START);
|
||||
contentPane.add(listPane, BorderLayout.CENTER);
|
||||
contentPane.add(buttonPane, BorderLayout.PAGE_END);
|
||||
getRootPane().setDefaultButton(openBtn);
|
||||
|
||||
searchField.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
|
||||
resultsList.requestFocus();
|
||||
if (!resultsModel.isEmpty()) {
|
||||
resultsList.setSelectedIndex(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setTitle(NLS.str("menu.search"));
|
||||
pack();
|
||||
setSize(700, 500);
|
||||
@@ -322,17 +348,16 @@ public class SearchDialog extends JDialog {
|
||||
}
|
||||
|
||||
private JCheckBox makeOptionsCheckBox(String name, final SearchOptions opt) {
|
||||
JCheckBox chBox = new JCheckBox(name);
|
||||
final JCheckBox chBox = new JCheckBox(name);
|
||||
chBox.setAlignmentX(LEFT_ALIGNMENT);
|
||||
chBox.setSelected(OPTIONS.contains(opt));
|
||||
chBox.addItemListener(new ItemListener() {
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
if (e.getStateChange() == ItemEvent.SELECTED) {
|
||||
if (chBox.isSelected()) {
|
||||
OPTIONS.add(opt);
|
||||
} else {
|
||||
OPTIONS.remove(opt);
|
||||
}
|
||||
loadData();
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package jadx.gui.utils;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class CacheObject {
|
||||
@Nullable
|
||||
private TextSearchIndex textIndex;
|
||||
|
||||
public void reset() {
|
||||
textIndex = null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TextSearchIndex getTextIndex() {
|
||||
return textIndex;
|
||||
}
|
||||
|
||||
public void setTextIndex(@Nullable TextSearchIndex textIndex) {
|
||||
this.textIndex = textIndex;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package jadx.gui.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class NameIndex<T> {
|
||||
|
||||
private final List<String> strings = new ArrayList<String>();
|
||||
private final List<T> objects = new ArrayList<T>();
|
||||
|
||||
public void add(String name, T obj) {
|
||||
strings.add(name);
|
||||
objects.add(obj);
|
||||
}
|
||||
|
||||
public List<T> search(String text) {
|
||||
List<T> results = new ArrayList<T>();
|
||||
int count = strings.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String name = strings.get(i);
|
||||
if (name.contains(text)) {
|
||||
results.add(objects.get(i));
|
||||
}
|
||||
}
|
||||
return results.isEmpty() ? Collections.<T>emptyList() : results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package jadx.gui.utils;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.api.JavaField;
|
||||
import jadx.api.JavaMethod;
|
||||
import jadx.gui.treemodel.CodeNode;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.StringReader;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFactory;
|
||||
import com.googlecode.concurrenttrees.suffix.ConcurrentSuffixTree;
|
||||
import com.googlecode.concurrenttrees.suffix.SuffixTree;
|
||||
|
||||
public class TextSearchIndex {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TextSearchIndex.class);
|
||||
|
||||
private SuffixTree<JNode> clsNamesTree;
|
||||
private SuffixTree<JNode> mthNamesTree;
|
||||
private SuffixTree<JNode> fldNamesTree;
|
||||
private SuffixTree<CodeNode> codeTree;
|
||||
|
||||
public TextSearchIndex() {
|
||||
clsNamesTree = new ConcurrentSuffixTree<JNode>(new DefaultCharArrayNodeFactory());
|
||||
mthNamesTree = new ConcurrentSuffixTree<JNode>(new DefaultCharArrayNodeFactory());
|
||||
fldNamesTree = new ConcurrentSuffixTree<JNode>(new DefaultCharArrayNodeFactory());
|
||||
codeTree = new ConcurrentSuffixTree<CodeNode>(new DefaultCharArrayNodeFactory());
|
||||
}
|
||||
|
||||
public void indexNames(JavaClass cls) {
|
||||
cls.decompile();
|
||||
clsNamesTree.put(cls.getFullName(), JNode.makeFrom(cls));
|
||||
for (JavaMethod mth : cls.getMethods()) {
|
||||
mthNamesTree.put(mth.getFullName(), JNode.makeFrom(mth));
|
||||
}
|
||||
for (JavaField fld : cls.getFields()) {
|
||||
fldNamesTree.put(fld.getFullName(), JNode.makeFrom(fld));
|
||||
}
|
||||
for (JavaClass innerCls : cls.getInnerClasses()) {
|
||||
indexNames(innerCls);
|
||||
}
|
||||
}
|
||||
|
||||
public void indexCode(JavaClass cls) {
|
||||
try {
|
||||
String code = cls.getCode();
|
||||
BufferedReader bufReader = new BufferedReader(new StringReader(code));
|
||||
String line;
|
||||
int lineNum = 0;
|
||||
while ((line = bufReader.readLine()) != null) {
|
||||
lineNum++;
|
||||
line = line.trim();
|
||||
if (!line.isEmpty()) {
|
||||
CodeNode node = new CodeNode(cls, lineNum, line);
|
||||
codeTree.put(line, node);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Failed to index class: {}", cls, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Iterable<JNode> searchClsName(String text) {
|
||||
return clsNamesTree.getValuesForKeysContaining(text);
|
||||
}
|
||||
|
||||
public Iterable<JNode> searchMthName(String text) {
|
||||
return mthNamesTree.getValuesForKeysContaining(text);
|
||||
}
|
||||
|
||||
public Iterable<JNode> searchFldName(String text) {
|
||||
return fldNamesTree.getValuesForKeysContaining(text);
|
||||
}
|
||||
|
||||
public Iterable<CodeNode> searchCode(String text) {
|
||||
return codeTree.getValuesForKeysContaining(text);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user