From bd4d4f49fff46492601bc7fe663c52089873218a Mon Sep 17 00:00:00 2001 From: Skylot Date: Mon, 13 Jul 2015 22:26:26 +0300 Subject: [PATCH] gui: add full text search (#74) --- jadx-gui/build.gradle | 1 + .../java/jadx/gui/treemodel/CodeNode.java | 46 +++++ .../src/main/java/jadx/gui/ui/MainWindow.java | 11 +- .../main/java/jadx/gui/ui/SearchDialog.java | 163 ++++++++++-------- .../main/java/jadx/gui/utils/CacheObject.java | 21 +++ .../main/java/jadx/gui/utils/NameIndex.java | 28 --- .../java/jadx/gui/utils/TextSearchIndex.java | 83 +++++++++ 7 files changed, 254 insertions(+), 99 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/utils/NameIndex.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/TextSearchIndex.java diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index e525d06cd..e12675394 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -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 { diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java new file mode 100644 index 000000000..5b0378c55 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java @@ -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(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 0a390ccb1..3de9f3495 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -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(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java index c9a94b7be..f6237b598 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java @@ -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 OPTIONS = - EnumSet.of(SearchOptions.CLASS, SearchOptions.METHOD, SearchOptions.FIELD); + private static final Set OPTIONS = EnumSet.allOf(SearchOptions.class); private final TabbedPane tabbedPane; private final JadxWrapper wrapper; - private NameIndex 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(); - 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 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 { - 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 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 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(); } }); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java new file mode 100644 index 000000000..aec65dec9 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -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; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/NameIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/NameIndex.java deleted file mode 100644 index 61db890a5..000000000 --- a/jadx-gui/src/main/java/jadx/gui/utils/NameIndex.java +++ /dev/null @@ -1,28 +0,0 @@ -package jadx.gui.utils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class NameIndex { - - private final List strings = new ArrayList(); - private final List objects = new ArrayList(); - - public void add(String name, T obj) { - strings.add(name); - objects.add(obj); - } - - public List search(String text) { - List results = new ArrayList(); - 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.emptyList() : results; - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/TextSearchIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/TextSearchIndex.java new file mode 100644 index 000000000..c56eabe6c --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/TextSearchIndex.java @@ -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 clsNamesTree; + private SuffixTree mthNamesTree; + private SuffixTree fldNamesTree; + private SuffixTree codeTree; + + public TextSearchIndex() { + clsNamesTree = new ConcurrentSuffixTree(new DefaultCharArrayNodeFactory()); + mthNamesTree = new ConcurrentSuffixTree(new DefaultCharArrayNodeFactory()); + fldNamesTree = new ConcurrentSuffixTree(new DefaultCharArrayNodeFactory()); + codeTree = new ConcurrentSuffixTree(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 searchClsName(String text) { + return clsNamesTree.getValuesForKeysContaining(text); + } + + public Iterable searchMthName(String text) { + return mthNamesTree.getValuesForKeysContaining(text); + } + + public Iterable searchFldName(String text) { + return fldNamesTree.getValuesForKeysContaining(text); + } + + public Iterable searchCode(String text) { + return codeTree.getValuesForKeysContaining(text); + } +}