Commit bd4d4f49 authored by Skylot's avatar Skylot

gui: add full text search (#74)

parent 5a24eac3
......@@ -8,6 +8,7 @@ dependencies {
compile 'com.fifesoft:rsyntaxtextarea:2.5.6'
compile 'com.google.code.gson:gson:2.3.1'
compile files('libs/jfontchooser-1.0.5.jar')
compile 'com.googlecode.concurrent-trees:concurrent-trees:2.4.0'
}
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;
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();
......
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,118 +38,133 @@ 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>();
TextSearchIndex index = cache.getTextIndex();
if (index != null) {
return;
}
index = new TextSearchIndex();
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() {
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);
}
private void openSelectedItem() {
int selectedId = resultsList.getSelectedIndex();
if (selectedId == -1) {
TextSearchIndex index = cache.getTextIndex();
if (index == null) {
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)) {
index.add(cls.getFullName(), cls);
resultsModel.addAll(index.searchClsName(text));
}
if (OPTIONS.contains(SearchOptions.METHOD)) {
for (JavaMethod mth : cls.getMethods()) {
index.add(mth.getFullName(), mth);
}
resultsModel.addAll(index.searchMthName(text));
}
if (OPTIONS.contains(SearchOptions.FIELD)) {
for (JavaField fld : cls.getFields()) {
index.add(fld.getFullName(), fld);
}
resultsModel.addAll(index.searchFldName(text));
}
if (OPTIONS.contains(SearchOptions.CODE)) {
String code = cls.getCode();
index.add(code, cls);
resultsModel.addAll(index.searchCode(text));
}
for (JavaClass innerCls : cls.getInnerClasses()) {
indexClass(innerCls);
LOG.info("Search returned {} results", resultsModel.size());
}
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> {
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();
}
});
......
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