gui: add find usage feature, run decompilation and index jobs in background (#74, #75)

This commit is contained in:
Skylot
2015-07-26 18:06:26 +03:00
parent 2d8d416483
commit bc73010d4e
33 changed files with 1521 additions and 295 deletions
@@ -0,0 +1,87 @@
package jadx.gui.jobs;
import jadx.gui.JadxWrapper;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class BackgroundJob {
private static final Logger LOG = LoggerFactory.getLogger(DecompileJob.class);
protected final JadxWrapper wrapper;
private final ThreadPoolExecutor executor;
private Future<Boolean> future;
public BackgroundJob(JadxWrapper wrapper, int threadsCount) {
this.wrapper = wrapper;
this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount);
}
public synchronized Future<Boolean> process() {
if (future != null) {
return future;
}
ExecutorService shutdownExecutor = Executors.newSingleThreadExecutor();
FutureTask<Boolean> task = new ShutdownTask();
shutdownExecutor.execute(task);
shutdownExecutor.shutdown();
future = task;
return future;
}
private class ShutdownTask extends FutureTask<Boolean> {
public ShutdownTask() {
super(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
runJob();
executor.shutdown();
return executor.awaitTermination(5, TimeUnit.MINUTES);
}
});
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
executor.shutdownNow();
return super.cancel(mayInterruptIfRunning);
}
}
protected abstract void runJob();
public abstract String getInfoString();
protected void addTask(Runnable runnable) {
executor.execute(runnable);
}
public void processAndWait() {
try {
process().get();
} catch (Exception e) {
LOG.error("BackgroundJob.processAndWait failed", e);
}
}
public synchronized boolean isComplete() {
try {
return future != null && future.isDone();
} catch (Exception e) {
LOG.error("BackgroundJob.isComplete failed", e);
return false;
}
}
public int getProgress() {
return (int) (executor.getCompletedTaskCount() * 100 / (double) executor.getTaskCount());
}
}
@@ -0,0 +1,99 @@
package jadx.gui.jobs;
import jadx.gui.ui.ProgressPanel;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.TextSearchIndex;
import jadx.gui.utils.Utils;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BackgroundWorker extends SwingWorker<Void, Void> {
private static final Logger LOG = LoggerFactory.getLogger(BackgroundWorker.class);
private final CacheObject cache;
private final ProgressPanel progressPane;
public BackgroundWorker(CacheObject cacheObject, ProgressPanel progressPane) {
this.cache = cacheObject;
this.progressPane = progressPane;
}
public void exec() {
if (isDone()) {
return;
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
progressPane.setVisible(true);
}
});
addPropertyChangeListener(progressPane);
execute();
}
public void stop() {
if (isDone()) {
return;
}
LOG.debug("Canceling background jobs ...");
cancel(false);
}
@Override
protected Void doInBackground() throws Exception {
try {
System.gc();
LOG.debug("Memory usage: Before decompile: {}", Utils.memoryInfo());
runJob(cache.getDecompileJob());
LOG.debug("Memory usage: Before index: {}", Utils.memoryInfo());
runJob(cache.getIndexJob());
LOG.debug("Memory usage: After index: {}", Utils.memoryInfo());
System.gc();
LOG.debug("Memory usage: After gc: {}", Utils.memoryInfo());
TextSearchIndex searchIndex = cache.getTextIndex();
if (cache.getIndexJob().isUseFastSearch()
&& searchIndex != null
&& searchIndex.getSkippedCount() > 0) {
LOG.warn("Indexing of some classes skipped, count: {}, low memory: {}",
searchIndex.getSkippedCount(), Utils.memoryInfo());
}
} catch (Exception e) {
LOG.error("Exception in background worker", e);
}
return null;
}
private void runJob(BackgroundJob job) {
if (isCancelled()) {
return;
}
progressPane.changeLabel(this, job.getInfoString());
Future<Boolean> future = job.process();
while (!future.isDone()) {
try {
setProgress(job.getProgress());
if (isCancelled()) {
future.cancel(false);
}
Thread.sleep(500);
} catch (Exception e) {
LOG.error("Background worker error", e);
}
}
}
@Override
protected void done() {
progressPane.setVisible(false);
}
}
@@ -0,0 +1,28 @@
package jadx.gui.jobs;
import jadx.api.JavaClass;
import jadx.gui.JadxWrapper;
public class DecompileJob extends BackgroundJob {
public DecompileJob(JadxWrapper wrapper, int threadsCount) {
super(wrapper, threadsCount);
}
protected void runJob() {
for (final JavaClass cls : wrapper.getClasses()) {
addTask(new Runnable() {
@Override
public void run() {
cls.decompile();
}
});
}
}
@Override
public String getInfoString() {
return "Decompiling: ";
}
}
@@ -0,0 +1,76 @@
package jadx.gui.jobs;
import jadx.api.JavaClass;
import jadx.core.codegen.CodeWriter;
import jadx.gui.JadxWrapper;
import jadx.gui.settings.JadxSettings;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.CodeLinesInfo;
import jadx.gui.utils.CodeUsageInfo;
import jadx.gui.utils.TextSearchIndex;
import jadx.gui.utils.Utils;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IndexJob extends BackgroundJob {
private static final Logger LOG = LoggerFactory.getLogger(IndexJob.class);
private final CacheObject cache;
private final boolean useFastSearch;
public IndexJob(JadxWrapper wrapper, JadxSettings settings, CacheObject cache) {
super(wrapper, settings.getThreadsCount());
this.useFastSearch = settings.isUseFastSearch();
this.cache = cache;
}
protected void runJob() {
final TextSearchIndex index = new TextSearchIndex();
final CodeUsageInfo usageInfo = new CodeUsageInfo();
cache.setTextIndex(index);
cache.setUsageInfo(usageInfo);
for (final JavaClass cls : wrapper.getClasses()) {
addTask(new Runnable() {
@Override
public void run() {
try {
index.indexNames(cls);
CodeLinesInfo linesInfo = new CodeLinesInfo(cls);
String[] lines = splitIntoLines(cls);
usageInfo.processClass(cls, linesInfo, lines);
if (useFastSearch && Utils.isFreeMemoryAvailable()) {
index.indexCode(cls, linesInfo, lines);
} else {
index.classCodeIndexSkipped(cls);
}
} catch (Exception e) {
LOG.error("Index error in class: {}", cls.getFullName(), e);
}
}
});
}
}
@NotNull
protected String[] splitIntoLines(JavaClass cls) {
String[] lines = cls.getCode().split(CodeWriter.NL);
int count = lines.length;
for (int i = 0; i < count; i++) {
lines[i] = lines[i].trim();
}
return lines;
}
@Override
public String getInfoString() {
return "Indexing: ";
}
public boolean isUseFastSearch() {
return useFastSearch;
}
}
@@ -2,7 +2,6 @@ package jadx.gui.settings;
import jadx.cli.JadxCLIArgs;
import javax.swing.JLabel;
import java.awt.Font;
import java.util.ArrayList;
import java.util.Arrays;
@@ -10,12 +9,14 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
public class JadxSettings extends JadxCLIArgs {
private static final String USER_HOME = System.getProperty("user.home");
private static final int RECENT_FILES_COUNT = 15;
private static final Font DEFAULT_FONT = new JLabel().getFont();
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
static final Set<String> SKIP_FIELDS = new HashSet<String>(Arrays.asList(
"files", "input", "outputDir", "verbose", "printHelp"
@@ -27,6 +28,7 @@ public class JadxSettings extends JadxCLIArgs {
private boolean checkForUpdates = true;
private List<String> recentFiles = new ArrayList<String>();
private String fontStr = "";
private boolean useFastSearch = false;
public void sync() {
JadxSettingsAdapter.store(this);
@@ -73,10 +75,8 @@ public class JadxSettings extends JadxCLIArgs {
}
public void addRecentFile(String filePath) {
if (recentFiles.contains(filePath)) {
return;
}
recentFiles.add(filePath);
recentFiles.remove(filePath);
recentFiles.add(0, filePath);
int count = recentFiles.size();
if (count > RECENT_FILES_COUNT) {
recentFiles.subList(0, count - RECENT_FILES_COUNT).clear();
@@ -136,6 +136,14 @@ public class JadxSettings extends JadxCLIArgs {
this.deobfuscationUseSourceNameAsAlias = useSourceNameAsAlias;
}
public boolean isUseFastSearch() {
return useFastSearch;
}
public void setUseFastSearch(boolean useFastSearch) {
this.useFastSearch = useFastSearch;
}
public Font getFont() {
if (fontStr.isEmpty()) {
return DEFAULT_FONT;
@@ -2,6 +2,7 @@ package jadx.gui.settings;
import jadx.gui.JadxGUI;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.prefs.Preferences;
@@ -25,7 +26,9 @@ public class JadxSettingsAdapter {
private static ExclusionStrategy EXCLUDE_FIELDS = new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes f) {
return JadxSettings.SKIP_FIELDS.contains(f.getName());
return JadxSettings.SKIP_FIELDS.contains(f.getName())
|| f.hasModifier(Modifier.PUBLIC)
|| f.hasModifier(Modifier.TRANSIENT);
}
@Override
@@ -239,6 +239,14 @@ public class JadxSettingsWindow extends JDialog {
}
});
JCheckBox fastSearch = new JCheckBox();
fastSearch.setSelected(settings.isUseFastSearch());
fastSearch.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
settings.setUseFastSearch(e.getStateChange() == ItemEvent.SELECTED);
}
});
SettingsGroup other = new SettingsGroup(NLS.str("preferences.other"));
other.addRow(NLS.str("preferences.check_for_updates"), update);
other.addRow(NLS.str("preferences.threads"), threadsCount);
@@ -248,6 +256,7 @@ public class JadxSettingsWindow extends JDialog {
other.addRow(NLS.str("preferences.cfg"), cfg);
other.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
other.addRow(NLS.str("preferences.font"), fontBtn);
other.addRow(NLS.str("preferences.fast_search"), fastSearch);
return other;
}
@@ -1,27 +1,50 @@
package jadx.gui.treemodel;
import jadx.api.JavaClass;
import jadx.gui.utils.Utils;
import jadx.api.JavaNode;
import javax.swing.Icon;
import javax.swing.ImageIcon;
public class CodeNode extends JClass {
public class CodeNode extends JNode {
private static final ImageIcon ICON = Utils.openIcon("file_obj");
private static final long serialVersionUID = 1658650786734966545L;
private final JNode jNode;
private final JClass jParent;
private final String line;
private final int lineNum;
public CodeNode(JavaClass javaClass, int lineNum, String line) {
super(javaClass, (JClass) makeFrom(javaClass.getDeclaringClass()));
public CodeNode(JavaNode javaNode, int lineNum, String line) {
this.jNode = makeFrom(javaNode);
this.jParent = jNode.getJParent();
this.line = line;
this.lineNum = lineNum;
}
@Override
public Icon getIcon() {
return ICON;
return jNode.getIcon();
}
@Override
public JavaNode getJavaNode() {
return jNode.getJavaNode();
}
@Override
public JClass getJParent() {
return getRootClass();
}
@Override
public JClass getRootClass() {
JClass parent = jParent;
if (parent != null) {
return parent.getRootClass();
}
if (jNode instanceof JClass) {
return (JClass) jNode;
}
return null;
}
@Override
@@ -29,18 +52,23 @@ public class CodeNode extends JClass {
return lineNum;
}
@Override
public String makeDescString() {
return line;
}
@Override
public boolean hasDescString() {
return true;
}
@Override
public String makeString() {
return getCls().getFullName() + ":" + lineNum + " " + line;
return jNode.makeLongString();
}
@Override
public String makeLongString() {
return makeString();
}
@Override
public String toString() {
return makeString();
}
}
@@ -3,6 +3,7 @@ package jadx.gui.treemodel;
import jadx.api.JavaClass;
import jadx.api.JavaField;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.core.dex.info.AccessInfo;
import jadx.gui.utils.NLS;
import jadx.gui.utils.Utils;
@@ -103,6 +104,11 @@ public class JClass extends JNode {
return ICON_CLASS_DEFAULT;
}
@Override
public JavaNode getJavaNode() {
return cls;
}
@Override
public JClass getJParent() {
return jParent;
@@ -116,6 +122,11 @@ public class JClass extends JNode {
return jParent.getRootClass();
}
@Override
public String getName() {
return cls.getName();
}
public String getFullName() {
return cls.getFullName();
}
@@ -1,6 +1,7 @@
package jadx.gui.treemodel;
import jadx.api.JavaField;
import jadx.api.JavaNode;
import jadx.core.dex.info.AccessInfo;
import jadx.gui.utils.OverlayIcon;
import jadx.gui.utils.Utils;
@@ -27,6 +28,11 @@ public class JField extends JNode {
this.jParent = jClass;
}
@Override
public JavaNode getJavaNode() {
return field;
}
@Override
public JClass getJParent() {
return jParent;
@@ -64,4 +70,14 @@ public class JField extends JNode {
public String makeLongString() {
return Utils.typeFormat(field.getFullName(), field.getType());
}
@Override
public int hashCode() {
return field.hashCode();
}
@Override
public boolean equals(Object o) {
return this == o || o instanceof JField && field.equals(((JField) o).field);
}
}
@@ -1,6 +1,7 @@
package jadx.gui.treemodel;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.gui.utils.OverlayIcon;
@@ -29,11 +30,20 @@ public class JMethod extends JNode {
this.jParent = jClass;
}
@Override
public JavaNode getJavaNode() {
return mth;
}
@Override
public JClass getJParent() {
return jParent;
}
public ArgType getReturnType() {
return mth.getReturnType();
}
@Override
public JClass getRootClass() {
return jParent.getRootClass();
@@ -57,7 +67,7 @@ public class JMethod extends JNode {
return icon;
}
private String makeBaseString() {
String makeBaseString() {
if (mth.isClassInit()) {
return "{...}";
}
@@ -80,12 +90,22 @@ public class JMethod extends JNode {
@Override
public String makeString() {
return Utils.typeFormat(makeBaseString(), mth.getReturnType());
return Utils.typeFormat(makeBaseString(), getReturnType());
}
@Override
public String makeLongString() {
String name = mth.getDeclaringClass().getFullName() + "." + makeBaseString();
return Utils.typeFormat(name, mth.getReturnType());
return Utils.typeFormat(name, getReturnType());
}
@Override
public int hashCode() {
return mth.hashCode();
}
@Override
public boolean equals(Object o) {
return this == o || o instanceof JMethod && mth.equals(((JMethod) o).mth);
}
}
@@ -13,20 +13,20 @@ import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
public abstract class JNode extends DefaultMutableTreeNode {
public static JNode makeFrom(JavaNode node) {
if (node == null) {
return null;
}
if (node instanceof JavaClass) {
JClass p = (JClass) makeFrom(node.getDeclaringClass());
return new JClass((JavaClass) node, p);
}
if (node instanceof JavaMethod) {
JavaMethod mth = (JavaMethod) node;
return new JMethod(mth, new JClass(mth.getDeclaringClass()));
return new JMethod(mth, (JClass) makeFrom(mth.getDeclaringClass()));
}
if (node instanceof JavaField) {
JavaField fld = (JavaField) node;
return new JField(fld, new JClass(fld.getDeclaringClass()));
}
if (node == null) {
return null;
return new JField(fld, (JClass) makeFrom(fld.getDeclaringClass()));
}
throw new JadxRuntimeException("Unknown type for JavaNode: " + node.getClass());
}
@@ -40,6 +40,10 @@ public abstract class JNode extends DefaultMutableTreeNode {
return null;
}
public JavaNode getJavaNode() {
return null;
}
public String getContent() {
return null;
}
@@ -58,8 +62,24 @@ public abstract class JNode extends DefaultMutableTreeNode {
public abstract Icon getIcon();
public String getName() {
JavaNode javaNode = getJavaNode();
if (javaNode == null) {
return null;
}
return javaNode.getName();
}
public abstract String makeString();
public String makeDescString() {
return null;
}
public boolean hasDescString() {
return false;
}
public String makeLongString() {
return makeString();
}
@@ -9,6 +9,8 @@ import javax.swing.ImageIcon;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.NotNull;
public class JPackage extends JNode implements Comparable<JPackage> {
private static final long serialVersionUID = -4120718634156839804L;
@@ -45,6 +47,7 @@ public class JPackage extends JNode implements Comparable<JPackage> {
}
}
@Override
public String getName() {
return name;
}
@@ -77,7 +80,7 @@ public class JPackage extends JNode implements Comparable<JPackage> {
}
@Override
public int compareTo(JPackage o) {
public int compareTo(@NotNull JPackage o) {
return name.compareTo(o.name);
}
@@ -24,7 +24,7 @@ public class JResource extends JNode implements Comparable<JResource> {
private static final ImageIcon JAVA_ICON = Utils.openIcon("java_ovr");
private static final ImageIcon ERROR_ICON = Utils.openIcon("error_co");
public static enum JResType {
public enum JResType {
ROOT,
DIR,
FILE
@@ -0,0 +1,394 @@
package jadx.gui.ui;
import jadx.gui.jobs.BackgroundJob;
import jadx.gui.jobs.DecompileJob;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.TextNode;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.NLS;
import jadx.gui.utils.Position;
import jadx.gui.utils.TextSearchIndex;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingWorker;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rtextarea.SearchContext;
import org.fife.ui.rtextarea.SearchEngine;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class CommonSearchDialog extends JDialog {
private static final Logger LOG = LoggerFactory.getLogger(CommonSearchDialog.class);
private static final long serialVersionUID = 8939332306115370276L;
public static final int MAX_RESULTS_COUNT = 1000;
protected final TabbedPane tabbedPane;
protected final CacheObject cache;
protected final MainWindow mainWindow;
protected final Font codeFont;
protected ResultsModel resultsModel;
protected ResultsTable resultsTable;
protected JLabel warnLabel;
protected ProgressPanel progressPane;
protected String highlightText;
public CommonSearchDialog(MainWindow mainWindow) {
super(mainWindow);
this.mainWindow = mainWindow;
this.tabbedPane = mainWindow.getTabbedPane();
this.cache = mainWindow.getCacheObject();
this.codeFont = mainWindow.getSettings().getFont();
}
public void prepare() {
if (cache.getIndexJob().isComplete()) {
loadFinishedCommon();
loadFinished();
return;
}
LoadTask task = new LoadTask();
task.addPropertyChangeListener(progressPane);
task.execute();
}
protected void openSelectedItem() {
int selectedId = resultsTable.getSelectedRow();
if (selectedId == -1) {
return;
}
JNode node = (JNode) resultsModel.getValueAt(selectedId, 0);
tabbedPane.codeJump(new Position(node.getRootClass(), node.getLine()));
dispose();
}
protected void initCommon() {
KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
getRootPane().registerKeyboardAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dispose();
}
}, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
}
@NotNull
protected JPanel initButtonsPanel() {
progressPane = new ProgressPanel(mainWindow, false);
JButton cancelButton = new JButton(NLS.str("search_dialog.cancel"));
cancelButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
dispose();
}
});
JButton openBtn = new JButton(NLS.str("search_dialog.open"));
openBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
openSelectedItem();
}
});
getRootPane().setDefaultButton(openBtn);
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
buttonPane.add(progressPane);
buttonPane.add(Box.createRigidArea(new Dimension(5, 0)));
buttonPane.add(Box.createHorizontalGlue());
buttonPane.add(openBtn);
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
buttonPane.add(cancelButton);
return buttonPane;
}
protected JPanel initResultsTable() {
resultsModel = new ResultsModel();
resultsTable = new ResultsTable(resultsModel);
resultsTable.setShowHorizontalLines(false);
// resultsTable.setAutoCreateColumnsFromModel(true);
resultsTable.setDragEnabled(false);
resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
resultsTable.setBackground(ContentArea.BACKGROUND);
resultsTable.setColumnSelectionAllowed(false);
resultsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
resultsTable.setAutoscrolls(false);
resultsTable.setDefaultRenderer(Object.class, new ResultsTableCellRenderer());
resultsTable.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent evt) {
if (evt.getClickCount() == 2) {
openSelectedItem();
}
}
});
resultsTable.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
openSelectedItem();
}
}
});
warnLabel = new JLabel();
warnLabel.setForeground(Color.RED);
warnLabel.setVisible(false);
JPanel resultsPanel = new JPanel();
resultsPanel.setLayout(new BoxLayout(resultsPanel, BoxLayout.PAGE_AXIS));
resultsPanel.add(warnLabel);
resultsPanel.add(new JScrollPane(resultsTable,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
resultsPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
return resultsPanel;
}
protected static class ResultsTable extends JTable {
private static final long serialVersionUID = 3901184054736618969L;
public ResultsTable(ResultsModel resultsModel) {
super(resultsModel);
}
public void updateTable() {
ResultsModel model = (ResultsModel) getModel();
TableColumnModel columnModel = getColumnModel();
int width = getParent().getWidth();
int firstColMaxWidth = (int) (width * 0.5);
int rowCount = getRowCount();
int columnCount = getColumnCount();
if (!model.isAddDescColumn()) {
firstColMaxWidth = width;
}
Component codeComp = null;
for (int col = 0; col < columnCount; col++) {
int colWidth = 50;
for (int row = 0; row < rowCount; row++) {
TableCellRenderer renderer = getCellRenderer(row, col);
Component comp = prepareRenderer(renderer, row, col);
if (comp == null) {
continue;
}
colWidth = Math.max(comp.getPreferredSize().width, colWidth);
if (codeComp == null && col == 1) {
codeComp = comp;
}
}
colWidth += 10;
if (col == 0) {
colWidth = Math.min(colWidth, firstColMaxWidth);
} else {
colWidth = Math.max(colWidth, width - columnModel.getColumn(0).getPreferredWidth());
}
TableColumn column = columnModel.getColumn(col);
column.setPreferredWidth(colWidth);
}
if (codeComp != null) {
setRowHeight(codeComp.getPreferredSize().height + 4);
}
updateUI();
}
}
protected static class ResultsModel extends AbstractTableModel {
private static final long serialVersionUID = -7821286846923903208L;
private static final String[] COLUMN_NAMES = {"Node", "Code"};
private final List<JNode> rows = new ArrayList<JNode>();
private boolean addDescColumn;
protected void addAll(Iterable<? extends JNode> nodes) {
for (JNode node : nodes) {
int size = getRowCount();
if (size >= MAX_RESULTS_COUNT) {
if (size == MAX_RESULTS_COUNT) {
add(new TextNode("Search results truncated (limit: " + MAX_RESULTS_COUNT + ")"));
}
return;
}
add(node);
}
}
private void add(JNode node) {
if (node.hasDescString()) {
addDescColumn = true;
}
rows.add(node);
}
public void clear() {
addDescColumn = false;
rows.clear();
}
public boolean isAddDescColumn() {
return addDescColumn;
}
@Override
public int getRowCount() {
return rows.size();
}
@Override
public int getColumnCount() {
return 2;
}
@Override
public String getColumnName(int index) {
return COLUMN_NAMES[index];
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
return rows.get(rowIndex);
}
}
protected class ResultsTableCellRenderer implements TableCellRenderer {
private final Color selectedBackground;
private final Color selectedForeground;
ResultsTableCellRenderer() {
UIDefaults defaults = UIManager.getDefaults();
selectedBackground = defaults.getColor("List.selectionBackground");
selectedForeground = defaults.getColor("List.selectionForeground");
}
@Override
public Component getTableCellRendererComponent(JTable table, Object obj, boolean isSelected,
boolean hasFocus, int row, int column) {
if (!(obj instanceof JNode)) {
return null;
}
JNode node = (JNode) obj;
if (column == 0) {
JLabel label = new JLabel();
label.setOpaque(true);
if (isSelected) {
label.setBackground(selectedBackground);
label.setForeground(selectedForeground);
} else {
label.setBackground(ContentArea.BACKGROUND);
}
label.setIcon(node.getIcon());
label.setText(node.makeLongString() + " ");
return label;
}
if (node.hasDescString()) {
RSyntaxTextArea textArea = new RSyntaxTextArea();
textArea.setFont(codeFont);
textArea.setEditable(false);
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
textArea.setBackground(isSelected ? selectedBackground : ContentArea.BACKGROUND);
textArea.setText(" " + node.makeDescString());
textArea.setRows(1);
textArea.setColumns(textArea.getText().length());
if (highlightText != null) {
SearchEngine.markAll(textArea, new SearchContext(highlightText));
}
return textArea;
}
return null;
}
}
private class LoadTask extends SwingWorker<Void, Void> {
public LoadTask() {
loadStartCommon();
loadStart();
}
@Override
public Void doInBackground() {
try {
mainWindow.getBackgroundWorker().exec();
DecompileJob decompileJob = cache.getDecompileJob();
progressPane.changeLabel(this, decompileJob.getInfoString());
decompileJob.processAndWait();
BackgroundJob indexJob = cache.getIndexJob();
progressPane.changeLabel(this, indexJob.getInfoString());
indexJob.processAndWait();
} catch (Exception e) {
LOG.error("Waiting background tasks failed", e);
}
return null;
}
@Override
public void done() {
loadFinishedCommon();
loadFinished();
}
}
protected void loadStartCommon() {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
progressPane.setIndeterminate(true);
progressPane.setVisible(true);
resultsTable.setEnabled(false);
warnLabel.setVisible(false);
}
private void loadFinishedCommon() {
setCursor(null);
resultsTable.setEnabled(true);
progressPane.setVisible(false);
TextSearchIndex textIndex = cache.getTextIndex();
if (textIndex == null) {
warnLabel.setText("Index not initialized, search will be disabled!");
warnLabel.setVisible(true);
}
}
protected abstract void loadFinished();
protected abstract void loadStart();
}
@@ -1,15 +1,21 @@
package jadx.gui.ui;
import jadx.api.CodePosition;
import jadx.api.JavaNode;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.utils.Position;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JPopupMenu;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.DefaultCaret;
@@ -17,6 +23,7 @@ import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import org.fife.ui.rsyntaxtextarea.LinkGenerator;
import org.fife.ui.rsyntaxtextarea.LinkGeneratorResult;
@@ -63,11 +70,23 @@ class ContentArea extends RSyntaxTextArea {
CodeLinkGenerator codeLinkProcessor = new CodeLinkGenerator((JClass) node);
setLinkGenerator(codeLinkProcessor);
addHyperlinkListener(codeLinkProcessor);
addMenuItems(this, (JClass) node);
}
setText(node.getContent());
}
private void addMenuItems(ContentArea contentArea, JClass jCls) {
Action findUsage = new FindUsageAction(contentArea, jCls);
// TODO: hotkey works only when popup menu is shown
// findUsage.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_F7, KeyEvent.ALT_DOWN_MASK));
JPopupMenu popup = getPopupMenu();
popup.addSeparator();
popup.add(findUsage);
popup.addPopupMenuListener((PopupMenuListener) findUsage);
}
public void loadSettings() {
JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings();
setFont(settings.getFont());
@@ -83,7 +102,7 @@ class ContentArea extends RSyntaxTextArea {
}
}
if (node instanceof JClass) {
Position pos = getPosition((JClass) node, this, token.getOffset());
Position pos = getDefPosition((JClass) node, this, token.getOffset());
if (pos != null) {
return true;
}
@@ -100,21 +119,30 @@ class ContentArea extends RSyntaxTextArea {
return super.getForegroundForToken(t);
}
static Position getPosition(JClass jCls, RSyntaxTextArea textArea, int offset) {
static Position getDefPosition(JClass jCls, RSyntaxTextArea textArea, int offset) {
JavaNode node = getJavaNodeAtOffset(jCls, textArea, offset);
if (node == null) {
return null;
}
CodePosition pos = jCls.getCls().getDefinitionPosition(node);
if (pos == null) {
return null;
}
return new Position(pos);
}
static JavaNode getJavaNodeAtOffset(JClass jCls, RSyntaxTextArea textArea, int offset) {
try {
int line = textArea.getLineOfOffset(offset);
int lineOffset = offset - textArea.getLineStartOffset(line);
CodePosition pos = jCls.getCls().getDefinitionPosition(line + 1, lineOffset + 1);
if (pos != null && pos.isSet()) {
return new Position(pos);
}
return jCls.getCls().getJavaNodeAtPosition(line + 1, lineOffset + 1);
} catch (BadLocationException e) {
LOG.error("Can't get line by offset", e);
LOG.error("Can't get java node by offset", e);
}
return null;
}
Position getCurrentPosition() {
public Position getCurrentPosition() {
return new Position(node, getCaretLineNumber() + 1);
}
@@ -166,6 +194,52 @@ class ContentArea extends RSyntaxTextArea {
}
}
private class FindUsageAction extends AbstractAction implements PopupMenuListener {
private static final long serialVersionUID = 4692546569977976384L;
private final ContentArea contentArea;
private final JClass jCls;
private JavaNode node;
public FindUsageAction(ContentArea contentArea, JClass jCls) {
super("Find Usage");
this.contentArea = contentArea;
this.jCls = jCls;
}
@Override
public void actionPerformed(ActionEvent e) {
if (node == null) {
return;
}
MainWindow mainWindow = contentPanel.getTabbedPane().getMainWindow();
UsageDialog usageDialog = new UsageDialog(mainWindow, JNode.makeFrom(node));
usageDialog.setVisible(true);
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
node = null;
Point pos = contentArea.getMousePosition();
if (pos != null) {
Token token = contentArea.viewToToken(pos);
if (token != null) {
node = getJavaNodeAtOffset(jCls, contentArea, token.getOffset());
}
}
setEnabled(node != null);
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
}
}
private class CodeLinkGenerator implements LinkGenerator, HyperlinkListener {
private final JClass jCls;
@@ -181,7 +255,7 @@ class ContentArea extends RSyntaxTextArea {
return null;
}
final int sourceOffset = token.getOffset();
final Position defPos = getPosition(jCls, textArea, sourceOffset);
final Position defPos = getDefPosition(jCls, textArea, sourceOffset);
if (defPos == null) {
return null;
}
@@ -207,12 +281,7 @@ class ContentArea extends RSyntaxTextArea {
public void hyperlinkUpdate(HyperlinkEvent e) {
Object obj = e.getSource();
if (obj instanceof Position) {
Position pos = (Position) obj;
LOG.debug("Code jump to: {}", pos);
TabbedPane tabbedPane = contentPanel.getTabbedPane();
tabbedPane.getJumpManager().addPosition(getCurrentPosition());
tabbedPane.getJumpManager().addPosition(pos);
tabbedPane.showCode(pos);
contentPanel.getTabbedPane().codeJump((Position) obj);
}
}
}
@@ -23,7 +23,6 @@ public class MainDropTarget implements DropTargetListener {
private final MainWindow mainWindow;
public MainDropTarget(MainWindow mainWindow) {
super();
this.mainWindow = mainWindow;
}
@@ -50,6 +49,7 @@ public class MainDropTarget implements DropTargetListener {
}
@Override
@SuppressWarnings("unchecked")
public void drop(DropTargetDropEvent dtde) {
if (!dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
dtde.rejectDrop();
@@ -57,7 +57,6 @@ public class MainDropTarget implements DropTargetListener {
}
dtde.acceptDrop(dtde.getDropAction());
try {
Transferable transferable = dtde.getTransferable();
List<File> transferData = (List<File>) transferable.getTransferData(DataFlavor.javaFileListFlavor);
if (transferData != null && transferData.size() > 0) {
@@ -65,7 +64,6 @@ public class MainDropTarget implements DropTargetListener {
// load first file
mainWindow.openFile(transferData.get(0));
}
} catch (Exception e) {
LOG.error("File drop operation failed", e);
}
@@ -73,7 +71,5 @@ public class MainDropTarget implements DropTargetListener {
@Override
public void dragExit(DropTargetEvent dte) {
}
}
@@ -1,6 +1,9 @@
package jadx.gui.ui;
import jadx.gui.JadxWrapper;
import jadx.gui.jobs.BackgroundWorker;
import jadx.gui.jobs.DecompileJob;
import jadx.gui.jobs.IndexJob;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.JadxSettingsWindow;
import jadx.gui.treemodel.JClass;
@@ -48,7 +51,6 @@ import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.DisplayMode;
@@ -66,6 +68,8 @@ import java.awt.event.MouseEvent;
import java.io.File;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -111,7 +115,9 @@ public class MainWindow extends JFrame {
private JToggleButton deobfToggleBtn;
private boolean isFlattenPackage;
private Link updateLink;
private ProgressPanel progressPane;
private BackgroundWorker backgroundWorker;
private DropTarget dropTarget;
public MainWindow(JadxSettings settings) {
@@ -119,6 +125,7 @@ public class MainWindow extends JFrame {
this.settings = settings;
this.cacheObject = new CacheObject();
resetCache();
initUI();
initMenuAndToolbar();
checkForUpdate();
@@ -175,18 +182,45 @@ public class MainWindow extends JFrame {
}
public void openFile(File file) {
cacheObject.reset();
tabbedPane.closeAllTabs();
resetCache();
wrapper.openFile(file);
deobfToggleBtn.setSelected(settings.isDeobfuscationOn());
settings.addRecentFile(file.getAbsolutePath());
initTree();
setTitle(DEFAULT_TITLE + " - " + file.getName());
runBackgroundJobs();
}
protected void resetCache() {
cacheObject.reset();
int threadsCount = settings.getThreadsCount();
cacheObject.setDecompileJob(new DecompileJob(wrapper, threadsCount));
cacheObject.setIndexJob(new IndexJob(wrapper, settings, cacheObject));
}
private synchronized void runBackgroundJobs() {
cancelBackgroundJobs();
backgroundWorker = new BackgroundWorker(cacheObject, progressPane);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
backgroundWorker.exec();
}
}, 1000);
}
public synchronized void cancelBackgroundJobs() {
if (backgroundWorker != null) {
backgroundWorker.stop();
backgroundWorker = new BackgroundWorker(cacheObject, progressPane);
resetCache();
}
}
public void reOpenFile() {
File openedFile = wrapper.getOpenFile();
if (openedFile != null) {
tabbedPane.closeAllTabs();
openFile(openedFile);
}
}
@@ -247,14 +281,14 @@ public class MainWindow extends JFrame {
if (obj instanceof JResource) {
JResource res = (JResource) obj;
if (res.getContent() != null) {
tabbedPane.showCode(new Position(res, res.getLine()));
tabbedPane.codeJump(new Position(res, res.getLine()));
}
}
if (obj instanceof JNode) {
JNode node = (JNode) obj;
JClass cls = node.getRootClass();
if (cls != null) {
tabbedPane.showCode(new Position(cls, node.getLine()));
tabbedPane.codeJump(new Position(cls, node.getLine()));
}
}
} catch (Exception e) {
@@ -539,14 +573,18 @@ public class MainWindow extends JFrame {
}
});
JScrollPane treeScrollPane = new JScrollPane(tree);
splitPane.setLeftComponent(treeScrollPane);
progressPane = new ProgressPanel(this, true);
JPanel leftPane = new JPanel(new BorderLayout());
leftPane.add(new JScrollPane(tree), BorderLayout.CENTER);
leftPane.add(progressPane, BorderLayout.PAGE_END);
splitPane.setLeftComponent(leftPane);
tabbedPane = new TabbedPane(this);
splitPane.setRightComponent(tabbedPane);
dropTarget = new DropTarget(this, DnDConstants.ACTION_COPY, new MainDropTarget(this));
setContentPane(mainPanel);
setTitle(DEFAULT_TITLE);
}
@@ -565,6 +603,12 @@ public class MainWindow extends JFrame {
tabbedPane.loadSettings();
}
@Override
public void dispose() {
cancelBackgroundJobs();
super.dispose();
}
public JadxWrapper getWrapper() {
return wrapper;
}
@@ -581,6 +625,10 @@ public class MainWindow extends JFrame {
return cacheObject;
}
public BackgroundWorker getBackgroundWorker() {
return backgroundWorker;
}
private class RecentFilesMenuListener implements MenuListener {
private final JMenu recentFiles;
@@ -619,4 +667,5 @@ public class MainWindow extends JFrame {
public void menuCanceled(MenuEvent e) {
}
}
}
@@ -0,0 +1,82 @@
package jadx.gui.ui;
import jadx.gui.utils.Utils;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
public class ProgressPanel extends JPanel implements PropertyChangeListener {
private static final long serialVersionUID = -3238438119672015733L;
private static final Icon ICON_CANCEL = Utils.openIcon("cross");
private final JProgressBar progressBar;
private final JLabel progressLabel;
public ProgressPanel(final MainWindow mainWindow, boolean showCancelButton) {
progressLabel = new JLabel();
progressBar = new JProgressBar(0, 100);
progressBar.setIndeterminate(true);
progressBar.setStringPainted(false);
progressLabel.setLabelFor(progressBar);
setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setVisible(false);
add(progressLabel);
add(progressBar);
if (showCancelButton) {
JButton cancelButton = new JButton(ICON_CANCEL);
cancelButton.setPreferredSize(new Dimension(ICON_CANCEL.getIconWidth(), ICON_CANCEL.getIconHeight()));
cancelButton.setToolTipText("Cancel background jobs");
cancelButton.setBorderPainted(false);
cancelButton.setFocusPainted(false);
cancelButton.setContentAreaFilled(false);
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
mainWindow.cancelBackgroundJobs();
}
});
add(cancelButton);
}
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("progress".equals(evt.getPropertyName())) {
int progress = (Integer) evt.getNewValue();
progressBar.setIndeterminate(false);
progressBar.setValue(progress);
progressBar.setString(progress + "%");
progressBar.setStringPainted(true);
} else if ("label".equals(evt.getPropertyName())) {
setLabel((String) evt.getNewValue());
}
}
public void setLabel(String label) {
progressLabel.setText(label);
}
public void setIndeterminate(boolean newValue) {
progressBar.setIndeterminate(newValue);
}
public void changeLabel(SwingWorker<?, ?> task, String label) {
task.firePropertyChange("label", null, label);
}
}
@@ -1,53 +1,28 @@
package jadx.gui.ui;
import jadx.api.JavaClass;
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.Position;
import jadx.gui.utils.TextSearchIndex;
import jadx.gui.utils.TextStandardActions;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.WindowConstants;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
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.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.EnumSet;
@@ -56,12 +31,11 @@ import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SearchDialog extends JDialog {
public class SearchDialog extends CommonSearchDialog {
private static final long serialVersionUID = -5105405456969134105L;
private static final Logger LOG = LoggerFactory.getLogger(SearchDialog.class);
private static final int MAX_RESULTS_COUNT = 500;
enum SearchOptions {
CLASS,
@@ -72,67 +46,43 @@ public class SearchDialog extends JDialog {
private Set<SearchOptions> options = EnumSet.allOf(SearchOptions.class);
private final TabbedPane tabbedPane;
private final JadxWrapper wrapper;
private final CacheObject cache;
private JTextField searchField;
private ResultsModel resultsModel;
private JList resultsList;
private JProgressBar busyBar;
public SearchDialog(MainWindow mainWindow, Set<SearchOptions> options) {
super(mainWindow);
this.tabbedPane = mainWindow.getTabbedPane();
this.wrapper = mainWindow.getWrapper();
this.cache = mainWindow.getCacheObject();
this.options = options;
initUI();
addWindowListener(new WindowAdapter() {
@Override
public void windowActivated(WindowEvent e) {
public void windowOpened(WindowEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
prepare();
searchField.requestFocus();
openInit();
}
});
}
});
}
public void prepare() {
TextSearchIndex index = cache.getTextIndex();
if (index != null) {
return;
protected void openInit() {
prepare();
String lastSearch = cache.getLastSearch();
if (lastSearch != null) {
searchField.setText(lastSearch);
searchField.selectAll();
}
LoadTask task = new LoadTask();
task.execute();
}
private void loadData() {
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);
searchField.requestFocus();
}
private synchronized void performSearch() {
resultsModel.removeAllElements();
resultsModel.clear();
String text = searchField.getText();
if (text == null || text.isEmpty() || options.isEmpty()) {
return;
}
cache.setLastSearch(text);
TextSearchIndex index = cache.getTextIndex();
if (index == null) {
return;
@@ -149,92 +99,9 @@ public class SearchDialog extends JDialog {
if (options.contains(SearchOptions.CODE)) {
resultsModel.addAll(index.searchCode(text));
}
LOG.info("Search returned {} results", resultsModel.size());
highlightText = text;
resultsTable.updateTable();
}
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 LoadTask() {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
busyBar.setVisible(true);
searchField.setEnabled(false);
resultsList.setEnabled(false);
}
@Override
public Void doInBackground() {
loadData();
return null;
}
@Override
public void done() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
setCursor(null);
searchField.setEnabled(true);
resultsList.setEnabled(true);
busyBar.setVisible(false);
}
});
}
}
private static class ResultsModel extends DefaultListModel {
private static final long serialVersionUID = -7821286846923903208L;
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);
}
}
}
private static class ResultsCellRenderer implements ListCellRenderer {
private final Color selectedBackground;
private final Color selectedForeground;
ResultsCellRenderer() {
UIDefaults defaults = UIManager.getDefaults();
selectedBackground = defaults.getColor("List.selectionBackground");
selectedForeground = defaults.getColor("List.selectionForeground");
}
@Override
public Component getListCellRendererComponent(JList list,
Object obj, int index, boolean isSelected, boolean cellHasFocus) {
if (!(obj instanceof JNode)) {
return null;
}
JNode value = (JNode) obj;
JLabel label = new JLabel();
label.setOpaque(true);
label.setIcon(value.getIcon());
label.setText(value.makeLongString());
if (isSelected) {
label.setBackground(selectedBackground);
label.setForeground(selectedForeground);
}
return label;
}
}
private class SearchFieldListener implements DocumentListener {
public void changedUpdate(DocumentEvent e) {
@@ -263,17 +130,6 @@ public class SearchDialog extends JDialog {
JCheckBox fldChBox = makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD);
JCheckBox codeChBox = makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE);
resultsModel = new ResultsModel();
resultsList = new JList(resultsModel);
resultsList.setCellRenderer(new ResultsCellRenderer());
resultsList.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent evt) {
if (evt.getClickCount() == 2) {
openSelectedItem();
}
}
});
JPanel searchOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
searchOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in")));
searchOptions.add(clsChBox);
@@ -292,68 +148,30 @@ public class SearchDialog extends JDialog {
searchPane.add(searchOptions);
searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
JPanel listPane = new JPanel();
listPane.setLayout(new BoxLayout(listPane, BoxLayout.PAGE_AXIS));
listPane.add(new JScrollPane(resultsList));
listPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
busyBar = new JProgressBar();
busyBar.setIndeterminate(true);
busyBar.setVisible(false);
//Create and initialize the buttons.
JButton cancelButton = new JButton(NLS.str("search_dialog.cancel"));
cancelButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
dispose();
}
});
JButton openBtn = new JButton(NLS.str("search_dialog.open"));
openBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
openSelectedItem();
}
});
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
buttonPane.add(busyBar);
buttonPane.add(Box.createRigidArea(new Dimension(5, 0)));
buttonPane.add(Box.createHorizontalGlue());
buttonPane.add(openBtn);
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
buttonPane.add(cancelButton);
initCommon();
JPanel resultsPanel = initResultsTable();
JPanel buttonPane = initButtonsPanel();
Container contentPane = getContentPane();
contentPane.add(searchPane, BorderLayout.PAGE_START);
contentPane.add(listPane, BorderLayout.CENTER);
contentPane.add(resultsPanel, 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);
if (resultsModel.getRowCount() != 0) {
resultsTable.setRowSelectionInterval(0, 0);
}
resultsTable.requestFocus();
}
}
});
KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
getRootPane().registerKeyboardAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dispose();
}
}, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
setTitle(NLS.str("menu.text_search"));
pack();
setSize(700, 500);
setSize(800, 500);
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setModalityType(ModalityType.MODELESS);
@@ -375,4 +193,14 @@ public class SearchDialog extends JDialog {
});
return chBox;
}
@Override
protected void loadFinished() {
searchField.setEnabled(true);
}
@Override
protected void loadStart() {
searchField.setEnabled(false);
}
}
@@ -16,6 +16,7 @@ import javax.swing.JPopupMenu;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicButtonUI;
import javax.swing.text.BadLocationException;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
@@ -29,8 +30,13 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class TabbedPane extends JTabbedPane {
private static final Logger LOG = LoggerFactory.getLogger(TabbedPane.class);
private static final long serialVersionUID = -8833600618794570904L;
private static final ImageIcon ICON_CLOSE = Utils.openIcon("cross");
@@ -65,19 +71,46 @@ class TabbedPane extends JTabbedPane {
return mainWindow;
}
void showCode(final Position pos) {
private void showCode(final Position pos) {
final ContentPanel contentPanel = getCodePanel(pos.getNode());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setSelectedComponent(contentPanel);
ContentArea contentArea = contentPanel.getContentArea();
contentArea.scrollToLine(pos.getLine());
int line = pos.getLine();
if (line < 0) {
try {
line = 1 + contentArea.getLineOfOffset(-line);
} catch (BadLocationException e) {
LOG.error("Can't get line for: {}", pos, e);
line = pos.getNode().getLine();
}
}
contentArea.scrollToLine(line);
contentArea.requestFocus();
}
});
}
public void codeJump(Position pos) {
Position curPos = getCurrentPosition();
if (curPos != null) {
jumps.addPosition(curPos);
jumps.addPosition(pos);
}
showCode(pos);
}
@Nullable
private Position getCurrentPosition() {
ContentPanel selectedCodePanel = getSelectedCodePanel();
if (selectedCodePanel == null) {
return null;
}
return selectedCodePanel.getContentArea().getCurrentPosition();
}
public void navBack() {
Position pos = jumps.getPrev();
if (pos != null) {
@@ -116,6 +149,7 @@ class TabbedPane extends JTabbedPane {
return panel;
}
@Nullable
ContentPanel getSelectedCodePanel() {
return (ContentPanel) getSelectedComponent();
}
@@ -0,0 +1,101 @@
package jadx.gui.ui;
import jadx.gui.treemodel.JNode;
import jadx.gui.utils.CodeUsageInfo;
import jadx.gui.utils.NLS;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UsageDialog extends CommonSearchDialog {
private static final long serialVersionUID = -5105405789969134105L;
private static final Logger LOG = LoggerFactory.getLogger(UsageDialog.class);
private final JNode node;
public UsageDialog(MainWindow mainWindow, JNode node) {
super(mainWindow);
this.node = node;
initUI();
addWindowListener(new WindowAdapter() {
@Override
public void windowOpened(WindowEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
openInit();
}
});
}
});
}
protected void openInit() {
prepare();
}
@Override
protected void loadFinished() {
performSearch();
}
@Override
protected void loadStart() {
}
private synchronized void performSearch() {
resultsModel.clear();
CodeUsageInfo usageInfo = cache.getUsageInfo();
if (usageInfo == null) {
return;
}
resultsModel.addAll(usageInfo.getUsageList(node));
// TODO: highlight only needed node usage
highlightText = null;
resultsTable.updateTable();
}
private void initUI() {
JLabel lbl = new JLabel(NLS.str("usage_dialog.label"));
JLabel nodeLabel = new JLabel(this.node.makeLongString(), this.node.getIcon(), SwingConstants.LEFT);
lbl.setLabelFor(nodeLabel);
JPanel searchPane = new JPanel();
searchPane.setLayout(new FlowLayout(FlowLayout.LEFT));
searchPane.add(lbl);
searchPane.add(nodeLabel);
searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
initCommon();
JPanel resultsPanel = initResultsTable();
JPanel buttonPane = initButtonsPanel();
Container contentPane = getContentPane();
contentPane.add(searchPane, BorderLayout.PAGE_START);
contentPane.add(resultsPanel, BorderLayout.CENTER);
contentPane.add(buttonPane, BorderLayout.PAGE_END);
setTitle(NLS.str("usage_dialog.title"));
pack();
setSize(800, 500);
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
setModalityType(ModalityType.MODELESS);
}
}
@@ -1,13 +1,31 @@
package jadx.gui.utils;
import jadx.gui.jobs.DecompileJob;
import jadx.gui.jobs.IndexJob;
import org.jetbrains.annotations.Nullable;
public class CacheObject {
@Nullable
private DecompileJob decompileJob;
private IndexJob indexJob;
private TextSearchIndex textIndex;
private CodeUsageInfo usageInfo;
private String lastSearch;
public void reset() {
textIndex = null;
lastSearch = null;
usageInfo = null;
}
public DecompileJob getDecompileJob() {
return decompileJob;
}
public void setDecompileJob(DecompileJob decompileJob) {
this.decompileJob = decompileJob;
}
@Nullable
@@ -15,7 +33,33 @@ public class CacheObject {
return textIndex;
}
public void setTextIndex(@Nullable TextSearchIndex textIndex) {
public void setTextIndex(TextSearchIndex textIndex) {
this.textIndex = textIndex;
}
@Nullable
public String getLastSearch() {
return lastSearch;
}
public void setLastSearch(String lastSearch) {
this.lastSearch = lastSearch;
}
@Nullable
public CodeUsageInfo getUsageInfo() {
return usageInfo;
}
public void setUsageInfo(@Nullable CodeUsageInfo usageInfo) {
this.usageInfo = usageInfo;
}
public IndexJob getIndexJob() {
return indexJob;
}
public void setIndexJob(IndexJob indexJob) {
this.indexJob = indexJob;
}
}
@@ -0,0 +1,36 @@
package jadx.gui.utils;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
public class CodeLinesInfo {
private NavigableMap<Integer, JavaNode> map = new TreeMap<Integer, JavaNode>();
public CodeLinesInfo(JavaClass cls) {
addClass(cls);
}
public void addClass(JavaClass cls) {
map.put(cls.getDecompiledLine(), cls);
for (JavaClass innerCls : cls.getInnerClasses()) {
map.put(innerCls.getDecompiledLine(), innerCls);
addClass(innerCls);
}
for (JavaMethod mth : cls.getMethods()) {
map.put(mth.getDecompiledLine(), mth);
}
}
public JavaNode getJavaNodeByLine(int line) {
Map.Entry<Integer, JavaNode> entry = map.floorEntry(line);
if (entry == null) {
return null;
}
return entry.getValue();
}
}
@@ -0,0 +1,57 @@
package jadx.gui.utils;
import jadx.api.CodePosition;
import jadx.api.JavaClass;
import jadx.api.JavaNode;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CodeUsageInfo {
public static class UsageInfo {
private final List<CodeNode> usageList = new ArrayList<CodeNode>();
public List<CodeNode> getUsageList() {
return usageList;
}
}
private final Map<JNode, UsageInfo> usageMap = new HashMap<JNode, UsageInfo>();
public void processClass(JavaClass javaClass, CodeLinesInfo linesInfo, String[] lines) {
Map<CodePosition, JavaNode> usage = javaClass.getUsageMap();
for (Map.Entry<CodePosition, JavaNode> entry : usage.entrySet()) {
CodePosition codePosition = entry.getKey();
JavaNode javaNode = entry.getValue();
addUsage(JNode.makeFrom(javaNode), javaClass, linesInfo, codePosition, lines);
}
}
private void addUsage(JNode jNode, JavaClass javaClass,
CodeLinesInfo linesInfo, CodePosition codePosition, String[] lines) {
UsageInfo usageInfo = usageMap.get(jNode);
if (usageInfo == null) {
usageInfo = new UsageInfo();
usageMap.put(jNode, usageInfo);
}
int line = codePosition.getLine();
JavaNode javaNodeByLine = linesInfo.getJavaNodeByLine(line);
String codeLine = lines[line - 1].trim();
CodeNode codeNode = new CodeNode(javaNodeByLine == null ? javaClass : javaNodeByLine, line, codeLine);
usageInfo.getUsageList().add(codeNode);
}
public List<CodeNode> getUsageList(JNode node) {
UsageInfo usageInfo = usageMap.get(node);
if (usageInfo == null) {
return Collections.emptyList();
}
return usageInfo.getUsageList();
}
}
@@ -49,7 +49,7 @@ public class LogCollector extends CyclicBufferAppender<ILoggingEvent> {
public LogCollector() {
setName("LogCollector");
setMaxSize(50000);
setMaxSize(5000);
}
@Override
@@ -0,0 +1,28 @@
package jadx.gui.utils;
import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFactory;
import com.googlecode.concurrenttrees.suffix.ConcurrentSuffixTree;
public class SuffixTree<V> {
private final ConcurrentSuffixTree<V> tree;
public SuffixTree() {
this.tree = new ConcurrentSuffixTree<V>(new DefaultCharArrayNodeFactory());
}
public void put(String str, V value) {
if (str == null || str.isEmpty()) {
return;
}
tree.putIfAbsent(str, value);
}
public Iterable<V> getValuesForKeysContaining(String str) {
return tree.getValuesForKeysContaining(str);
}
public int size() {
return tree.size();
}
}
@@ -3,19 +3,18 @@ package jadx.gui.utils;
import jadx.api.JavaClass;
import jadx.api.JavaField;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.core.codegen.CodeWriter;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.CommonSearchDialog;
import java.io.BufferedReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
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);
@@ -25,15 +24,16 @@ public class TextSearchIndex {
private SuffixTree<JNode> fldNamesTree;
private SuffixTree<CodeNode> codeTree;
private List<JavaClass> skippedClasses = new ArrayList<JavaClass>();
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());
clsNamesTree = new SuffixTree<JNode>();
mthNamesTree = new SuffixTree<JNode>();
fldNamesTree = new SuffixTree<JNode>();
codeTree = new SuffixTree<CodeNode>();
}
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));
@@ -46,18 +46,15 @@ public class TextSearchIndex {
}
}
public void indexCode(JavaClass cls) {
public void indexCode(JavaClass cls, CodeLinesInfo linesInfo, String[] lines) {
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();
int count = lines.length;
for (int i = 0; i < count; i++) {
String line = lines[i];
if (!line.isEmpty()) {
CodeNode node = new CodeNode(cls, lineNum, line);
codeTree.put(line, node);
int lineNum = i + 1;
JavaNode node = linesInfo.getJavaNodeByLine(lineNum);
codeTree.put(line, new CodeNode(node == null ? cls : node, lineNum, line));
}
}
} catch (Exception e) {
@@ -78,6 +75,59 @@ public class TextSearchIndex {
}
public Iterable<CodeNode> searchCode(String text) {
return codeTree.getValuesForKeysContaining(text);
Iterable<CodeNode> items;
if (codeTree.size() > 0) {
items = codeTree.getValuesForKeysContaining(text);
if (skippedClasses.isEmpty()) {
return items;
}
} else {
items = null;
}
List<CodeNode> list = new ArrayList<CodeNode>();
if (items != null) {
for (CodeNode item : items) {
list.add(item);
}
}
addSkippedClasses(list, text);
return list;
}
private void addSkippedClasses(List<CodeNode> list, String text) {
for (JavaClass javaClass : skippedClasses) {
String code = javaClass.getCode();
int pos = 0;
while (pos != -1) {
pos = searchNext(list, text, javaClass, code, pos);
}
if (list.size() > CommonSearchDialog.MAX_RESULTS_COUNT) {
return;
}
}
}
private int searchNext(List<CodeNode> list, String text, JavaNode javaClass, String code, int startPos) {
int pos = code.indexOf(text, startPos);
if (pos == -1) {
return -1;
}
int lineStart = 1 + code.lastIndexOf(CodeWriter.NL, pos);
int lineEnd = code.indexOf(CodeWriter.NL, pos + text.length());
String line = code.substring(lineStart, lineEnd == -1 ? code.length() : lineEnd);
list.add(new CodeNode(javaClass, -pos, line.trim()));
return lineEnd;
}
public void classCodeIndexSkipped(JavaClass cls) {
this.skippedClasses.add(cls);
}
public List<JavaClass> getSkippedClasses() {
return skippedClasses;
}
public int getSkippedCount() {
return skippedClasses.size();
}
}
@@ -84,4 +84,30 @@ public class Utils {
}
return overIcon;
}
public static boolean isFreeMemoryAvailable() {
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory();
long totalFree = runtime.freeMemory() + maxMemory - runtime.totalMemory();
return totalFree > maxMemory * 0.2;
}
public static String memoryInfo() {
Runtime runtime = Runtime.getRuntime();
StringBuilder sb = new StringBuilder();
long maxMemory = runtime.maxMemory();
long allocatedMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
sb.append("free: ").append(format(freeMemory));
sb.append(", allocated: ").append(format(allocatedMemory));
sb.append(", max: ").append(format(maxMemory));
sb.append(", total free: ").append(format(freeMemory + maxMemory - allocatedMemory));
return sb.toString();
}
private static String format(long mem) {
return Long.toString((long) (mem / 1024. / 1024.)) + "MB";
}
}