Commit bd4d4f49 authored by Skylot's avatar Skylot

gui: add full text search (#74)

parent 5a24eac3
...@@ -8,6 +8,7 @@ dependencies { ...@@ -8,6 +8,7 @@ dependencies {
compile 'com.fifesoft:rsyntaxtextarea:2.5.6' compile 'com.fifesoft:rsyntaxtextarea:2.5.6'
compile 'com.google.code.gson:gson:2.3.1' compile 'com.google.code.gson:gson:2.3.1'
compile files('libs/jfontchooser-1.0.5.jar') compile files('libs/jfontchooser-1.0.5.jar')
compile 'com.googlecode.concurrent-trees:concurrent-trees:2.4.0'
} }
applicationDistribution.with { applicationDistribution.with {
......
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; ...@@ -10,6 +10,7 @@ import jadx.gui.treemodel.JRoot;
import jadx.gui.update.JadxUpdate; import jadx.gui.update.JadxUpdate;
import jadx.gui.update.JadxUpdate.IUpdateCallback; import jadx.gui.update.JadxUpdate.IUpdateCallback;
import jadx.gui.update.data.Release; import jadx.gui.update.data.Release;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.Link; import jadx.gui.utils.Link;
import jadx.gui.utils.NLS; import jadx.gui.utils.NLS;
import jadx.gui.utils.Position; import jadx.gui.utils.Position;
...@@ -88,6 +89,7 @@ public class MainWindow extends JFrame { ...@@ -88,6 +89,7 @@ public class MainWindow extends JFrame {
private final JadxWrapper wrapper; private final JadxWrapper wrapper;
private final JadxSettings settings; private final JadxSettings settings;
private final CacheObject cacheObject;
private JPanel mainPanel; private JPanel mainPanel;
...@@ -105,6 +107,7 @@ public class MainWindow extends JFrame { ...@@ -105,6 +107,7 @@ public class MainWindow extends JFrame {
public MainWindow(JadxSettings settings) { public MainWindow(JadxSettings settings) {
this.wrapper = new JadxWrapper(settings); this.wrapper = new JadxWrapper(settings);
this.settings = settings; this.settings = settings;
this.cacheObject = new CacheObject();
initUI(); initUI();
initMenuAndToolbar(); initMenuAndToolbar();
...@@ -162,6 +165,7 @@ public class MainWindow extends JFrame { ...@@ -162,6 +165,7 @@ public class MainWindow extends JFrame {
} }
public void openFile(File file) { public void openFile(File file) {
cacheObject.reset();
wrapper.openFile(file); wrapper.openFile(file);
deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); deobfToggleBtn.setSelected(settings.isDeobfuscationOn());
settings.addRecentFile(file.getAbsolutePath()); settings.addRecentFile(file.getAbsolutePath());
...@@ -355,10 +359,9 @@ public class MainWindow extends JFrame { ...@@ -355,10 +359,9 @@ public class MainWindow extends JFrame {
nav.add(search); nav.add(search);
ActionListener searchAction = new ActionListener() { ActionListener searchAction = new ActionListener() {
public void actionPerformed(ActionEvent event) { public void actionPerformed(ActionEvent event) {
final SearchDialog dialog = new SearchDialog(MainWindow.this, tabbedPane, wrapper);
dialog.prepare();
SwingUtilities.invokeLater(new Runnable() { SwingUtilities.invokeLater(new Runnable() {
public void run() { public void run() {
SearchDialog dialog = new SearchDialog(MainWindow.this, tabbedPane, wrapper);
dialog.setVisible(true); dialog.setVisible(true);
} }
}); });
...@@ -606,6 +609,10 @@ public class MainWindow extends JFrame { ...@@ -606,6 +609,10 @@ public class MainWindow extends JFrame {
return settings; return settings;
} }
public CacheObject getCacheObject() {
return cacheObject;
}
private class OpenListener implements ActionListener { private class OpenListener implements ActionListener {
public void actionPerformed(ActionEvent event) { public void actionPerformed(ActionEvent event) {
openFile(); openFile();
......
package jadx.gui.ui; package jadx.gui.ui;
import jadx.api.JavaClass; import jadx.api.JavaClass;
import jadx.api.JavaField;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.gui.JadxWrapper; import jadx.gui.JadxWrapper;
import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.TextNode;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.NLS; import jadx.gui.utils.NLS;
import jadx.gui.utils.NameIndex;
import jadx.gui.utils.Position; import jadx.gui.utils.Position;
import jadx.gui.utils.TextSearchIndex;
import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.TextStandardActions;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
...@@ -39,118 +38,133 @@ import java.awt.Container; ...@@ -39,118 +38,133 @@ import java.awt.Container;
import java.awt.Cursor; import java.awt.Cursor;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.FlowLayout; import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.awt.event.ItemEvent; import java.awt.event.ItemEvent;
import java.awt.event.ItemListener; import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; 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.EnumSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SearchDialog extends JDialog { public class SearchDialog extends JDialog {
private static final long serialVersionUID = -5105405456969134105L; 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, CLASS,
METHOD, METHOD,
FIELD, FIELD,
CODE CODE
} }
private static final Set<SearchOptions> OPTIONS = private static final Set<SearchOptions> OPTIONS = EnumSet.allOf(SearchOptions.class);
EnumSet.of(SearchOptions.CLASS, SearchOptions.METHOD, SearchOptions.FIELD);
private final TabbedPane tabbedPane; private final TabbedPane tabbedPane;
private final JadxWrapper wrapper; private final JadxWrapper wrapper;
private NameIndex<JavaNode> index; private final CacheObject cache;
private JTextField searchField; private JTextField searchField;
private ResultsModel resultsModel; private ResultsModel resultsModel;
private JList resultsList; private JList resultsList;
private JProgressBar busyBar; private JProgressBar busyBar;
public SearchDialog(Frame owner, TabbedPane tabbedPane, JadxWrapper wrapper) { public SearchDialog(MainWindow mainWindow, TabbedPane tabbedPane, JadxWrapper wrapper) {
super(owner); super(mainWindow);
this.tabbedPane = tabbedPane; this.tabbedPane = tabbedPane;
this.wrapper = wrapper; this.wrapper = wrapper;
this.cache = mainWindow.getCacheObject();
initUI(); initUI();
addWindowListener(new WindowAdapter() {
@Override
public void windowActivated(WindowEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
prepare();
}
});
}
});
} }
public void prepare() { public void prepare() {
TextSearchIndex index = cache.getTextIndex();
if (index != null) {
return;
}
LoadTask task = new LoadTask(); LoadTask task = new LoadTask();
task.init();
task.execute(); task.execute();
} }
private void loadData() { private void loadData() {
index = new NameIndex<JavaNode>(); TextSearchIndex index = cache.getTextIndex();
if (index != null) {
return;
}
index = new TextSearchIndex();
for (JavaClass cls : wrapper.getClasses()) { for (JavaClass cls : wrapper.getClasses()) {
indexClass(cls); index.indexNames(cls);
} }
for (JavaClass cls : wrapper.getClasses()) {
index.indexCode(cls);
}
cache.setTextIndex(index);
} }
private synchronized void performSearch() { private synchronized void performSearch() {
resultsModel.removeAllElements();
String text = searchField.getText(); String text = searchField.getText();
List<JavaNode> results; if (text == null || text.isEmpty() || OPTIONS.isEmpty()) {
if (text == null || text.isEmpty() || index == null) { return;
results = Collections.emptyList();
} else {
results = index.search(text);
} }
resultsModel.setResults(results); TextSearchIndex index = cache.getTextIndex();
} if (index == null) {
private void openSelectedItem() {
int selectedId = resultsList.getSelectedIndex();
if (selectedId == -1) {
return; return;
} }
JNode node = (JNode) resultsModel.get(selectedId);
tabbedPane.showCode(new Position(node.getRootClass(), node.getLine()));
dispose();
}
private void indexClass(JavaClass cls) {
if (OPTIONS.contains(SearchOptions.CLASS)) { if (OPTIONS.contains(SearchOptions.CLASS)) {
index.add(cls.getFullName(), cls); resultsModel.addAll(index.searchClsName(text));
} }
if (OPTIONS.contains(SearchOptions.METHOD)) { if (OPTIONS.contains(SearchOptions.METHOD)) {
for (JavaMethod mth : cls.getMethods()) { resultsModel.addAll(index.searchMthName(text));
index.add(mth.getFullName(), mth);
}
} }
if (OPTIONS.contains(SearchOptions.FIELD)) { if (OPTIONS.contains(SearchOptions.FIELD)) {
for (JavaField fld : cls.getFields()) { resultsModel.addAll(index.searchFldName(text));
index.add(fld.getFullName(), fld);
}
} }
if (OPTIONS.contains(SearchOptions.CODE)) { if (OPTIONS.contains(SearchOptions.CODE)) {
String code = cls.getCode(); resultsModel.addAll(index.searchCode(text));
index.add(code, cls);
} }
for (JavaClass innerCls : cls.getInnerClasses()) { LOG.info("Search returned {} results", resultsModel.size());
indexClass(innerCls); }
private void openSelectedItem() {
int selectedId = resultsList.getSelectedIndex();
if (selectedId == -1) {
return;
} }
JNode node = (JNode) resultsModel.get(selectedId);
tabbedPane.showCode(new Position(node.getRootClass(), node.getLine()));
dispose();
} }
private class LoadTask extends SwingWorker<Void, Void> { private class LoadTask extends SwingWorker<Void, Void> {
public void init() { public LoadTask() {
SwingUtilities.invokeLater(new Runnable() { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
public void run() { busyBar.setVisible(true);
busyBar.setVisible(true); searchField.setEnabled(false);
searchField.setEnabled(false); resultsList.setEnabled(false);
resultsList.setEnabled(false);
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
}
});
} }
@Override @Override
...@@ -175,14 +189,15 @@ public class SearchDialog extends JDialog { ...@@ -175,14 +189,15 @@ public class SearchDialog extends JDialog {
private static class ResultsModel extends DefaultListModel { private static class ResultsModel extends DefaultListModel {
private static final long serialVersionUID = -7821286846923903208L; private static final long serialVersionUID = -7821286846923903208L;
private void setResults(List<JavaNode> results) { private void addAll(Iterable<? extends JNode> nodes) {
removeAllElements(); for (JNode node : nodes) {
if (results.isEmpty()) { if (size() >= MAX_RESULTS_COUNT) {
return; if (size() == MAX_RESULTS_COUNT) {
} addElement(new TextNode("Search results truncated (limit: " + MAX_RESULTS_COUNT + ")"));
int count = Math.min(results.size(), MAX_RESULTS_COUNT); }
for (int i = 0; i < count; i++) { return;
addElement(JNode.makeFrom(results.get(i))); }
addElement(node);
} }
} }
} }
...@@ -243,7 +258,6 @@ public class SearchDialog extends JDialog { ...@@ -243,7 +258,6 @@ public class SearchDialog extends JDialog {
JCheckBox mthChBox = makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD); JCheckBox mthChBox = makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD);
JCheckBox fldChBox = makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD); JCheckBox fldChBox = makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD);
JCheckBox codeChBox = makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE); JCheckBox codeChBox = makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE);
codeChBox.setEnabled(false);
resultsModel = new ResultsModel(); resultsModel = new ResultsModel();
resultsList = new JList(resultsModel); resultsList = new JList(resultsModel);
...@@ -307,12 +321,24 @@ public class SearchDialog extends JDialog { ...@@ -307,12 +321,24 @@ public class SearchDialog extends JDialog {
buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
buttonPane.add(cancelButton); buttonPane.add(cancelButton);
Container contentPane = getContentPane(); final Container contentPane = getContentPane();
contentPane.add(searchPane, BorderLayout.PAGE_START); contentPane.add(searchPane, BorderLayout.PAGE_START);
contentPane.add(listPane, BorderLayout.CENTER); contentPane.add(listPane, BorderLayout.CENTER);
contentPane.add(buttonPane, BorderLayout.PAGE_END); contentPane.add(buttonPane, BorderLayout.PAGE_END);
getRootPane().setDefaultButton(openBtn); 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")); setTitle(NLS.str("menu.search"));
pack(); pack();
setSize(700, 500); setSize(700, 500);
...@@ -322,17 +348,16 @@ public class SearchDialog extends JDialog { ...@@ -322,17 +348,16 @@ public class SearchDialog extends JDialog {
} }
private JCheckBox makeOptionsCheckBox(String name, final SearchOptions opt) { private JCheckBox makeOptionsCheckBox(String name, final SearchOptions opt) {
JCheckBox chBox = new JCheckBox(name); final JCheckBox chBox = new JCheckBox(name);
chBox.setAlignmentX(LEFT_ALIGNMENT); chBox.setAlignmentX(LEFT_ALIGNMENT);
chBox.setSelected(OPTIONS.contains(opt)); chBox.setSelected(OPTIONS.contains(opt));
chBox.addItemListener(new ItemListener() { chBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) { public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) { if (chBox.isSelected()) {
OPTIONS.add(opt); OPTIONS.add(opt);
} else { } else {
OPTIONS.remove(opt); OPTIONS.remove(opt);
} }
loadData();
performSearch(); performSearch();
} }
}); });
......
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;
}
}
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;
}
}
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);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment