/*
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 2 of the license, or (at your option) any later version.
*/
package org.gjt.jclasslib.browser.detail.attributes.code;
import org.gjt.jclasslib.browser.BrowserHistory;
import org.gjt.jclasslib.browser.BrowserServices;
import org.gjt.jclasslib.browser.ConstantPoolHyperlinkListener;
import org.gjt.jclasslib.bytecode.*;
import org.gjt.jclasslib.io.ByteCodeReader;
import org.gjt.jclasslib.structures.ClassFile;
import org.gjt.jclasslib.structures.InvalidByteCodeException;
import org.gjt.jclasslib.structures.attributes.CodeAttribute;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.font.TextHitInfo;
import java.awt.font.TextLayout;
import java.io.IOException;
import java.text.AttributedString;
import java.util.*;
import java.util.List;
/**
Bytecode renderer.
@author <a href="mailto:jclasslib@ej-technologies.com">Ingo Kegel</a>
*/
public class ByteCodeDisplay extends JPanel implements Scrollable {
/** Horizontal margin. */
public static final int MARGIN_X = 3;
/** Vertical margin. */
public static final int MARGIN_Y = 3;
/** Border for the renderer. */
public static final Border BORDER = new EmptyBorder(MARGIN_Y, MARGIN_X, MARGIN_Y, MARGIN_X);
private static Map<TextAttribute, Object> STYLE_BASE;
private static Map<TextAttribute, Object> STYLE_NORMAL;
private static Map<TextAttribute, Object> STYLE_SMALL;
private static Map<TextAttribute, Object> STYLE_LINK;
private static Map<TextAttribute, Object> STYLE_OFFSET;
private static Map<TextAttribute, Object> STYLE_INSTRUCTION;
private static Map<TextAttribute, Object> STYLE_IMMEDIATE_VALUE;
private static final String TAB_STRING = " ";
static {
initStyles(null);
}
public static void initStyles(Font baseFont) {
STYLE_BASE = new HashMap<TextAttribute, Object>(2);
if (baseFont != null) {
STYLE_BASE.put(TextAttribute.FAMILY, baseFont.getFamily());
} else {
baseFont = UIManager.getFont("TextArea.font");
STYLE_BASE.put(TextAttribute.FAMILY, "MonoSpaced");
}
STYLE_BASE.put(TextAttribute.SIZE, (float)baseFont.getSize());
STYLE_NORMAL = new HashMap<TextAttribute, Object>(0);
STYLE_SMALL = new HashMap<TextAttribute, Object>(1);
STYLE_SMALL.put(TextAttribute.SIZE, (float)(baseFont.getSize() - 1));
STYLE_LINK = new HashMap<TextAttribute, Object>(3);
STYLE_LINK.put(TextAttribute.FOREGROUND, new Color(0, 128, 0));
STYLE_LINK.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
STYLE_LINK.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
STYLE_OFFSET = new HashMap<TextAttribute, Object>(1);
STYLE_OFFSET.put(TextAttribute.FOREGROUND, new Color(128, 0, 0));
STYLE_INSTRUCTION = new HashMap<TextAttribute, Object>(1);
STYLE_INSTRUCTION.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
STYLE_IMMEDIATE_VALUE = new HashMap<TextAttribute, Object>(2);
STYLE_IMMEDIATE_VALUE.put(TextAttribute.FOREGROUND, Color.magenta);
STYLE_IMMEDIATE_VALUE.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
}
private ByteCodeDetailPane detailPane;
private CodeAttribute codeAttribute;
private ClassFile classFile;
private int offsetWidth;
private String offsetBlank;
private HashMap<Integer, Integer> offsetToLine = new HashMap<Integer, Integer>();
private ArrayList<AttributedString> lines = new ArrayList<AttributedString>();
private ArrayList<String> textLines = new ArrayList<String>();
private TextLayout[] textLayouts;
private Map<Integer, BytecodeLink> lineToLink = new HashMap<Integer, BytecodeLink>();
private Set<AbstractInstruction> invalidBranches = new HashSet<AbstractInstruction>();
private LinkedList<LineCacheEntry> currentLineCache = new LinkedList<LineCacheEntry>();
private FontRenderContext frc;
private float currentHeight;
private float currentWidth;
private int lineHeight;
private int ascent;
private int characterWidth;
/**
* Get the left-padded value for a number.
* @param number the number
* @param width the total width.
* @return the padded string.
*/
public static String getPaddedValue(int number, int width) {
StringBuilder buffer = new StringBuilder();
String value = String.valueOf(number);
int valueLength = value.length();
for (int i = valueLength; i < width; i++) {
buffer.append(' ');
}
buffer.append(value);
return buffer.toString();
}
/**
* Constructor.
* @param detailPane the parent detail pane.
*/
public ByteCodeDisplay(ByteCodeDetailPane detailPane) {
this.detailPane = detailPane;
setupComponent();
setupEventHandlers();
}
// Scrollable
public Dimension getPreferredScrollableViewportSize() {
return null;
}
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
if (orientation == SwingConstants.HORIZONTAL) {
return 10;
} else {
if (lineHeight == 0) {
return 1;
}
int currentY = ((JViewport)getParent()).getViewPosition().y;
float line = 1f * (currentY - MARGIN_Y) / lineHeight;
int targetLine = (int)(direction < 0 ? Math.floor(line) - 1: Math.ceil(line) + 1);
int targetY = MARGIN_Y + targetLine * lineHeight + 1;
return Math.abs(currentY - targetY);
}
}
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
JViewport viewport = (JViewport)getParent();
if (orientation == SwingConstants.HORIZONTAL) {
return viewport.getWidth();
} else {
if (lineHeight == 0) {
return 1;
}
int currentY = viewport.getViewPosition().y;
int rawTargetY = currentY + (direction < 0 ? -1 : 1) * viewport.getHeight();
float line = 1f * (rawTargetY - MARGIN_Y) / lineHeight;
int targetLine = (int)(direction < 0 ? Math.ceil(line): Math.floor(line));
int targetY = MARGIN_Y + targetLine * lineHeight + 1;
return Math.abs(currentY - targetY);
}
}
public boolean getScrollableTracksViewportWidth() {
return false;
}
public boolean getScrollableTracksViewportHeight() {
return false;
}
// end Scrollable
/**
* Get the currently displayed code attribute.
* @return the code attribute.
*/
public CodeAttribute getCodeAttribute() {
return codeAttribute;
}
/**
* Get the current line count.
* @return the line count.
*/
public int getLineCount() {
return lines.size();
}
/**
* Get the current line height.
* @return the line height.
*/
public int getLineHeight() {
return lineHeight;
}
/**
* Get the current line ascent.
* @return the line ascent.
*/
public int getAscent() {
return ascent;
}
/**
* Set the code attribute that is to be displayed.
* @param codeAttribute the code attribute.
* @param classFile the class file of the code attribute.
*/
public void setCodeAttribute(CodeAttribute codeAttribute, ClassFile classFile) {
this.codeAttribute = codeAttribute;
this.classFile = classFile;
frc = ((Graphics2D)getGraphics()).getFontRenderContext();
setupTextLayouts();
invalidate();
}
/**
* Perform a link operation at a given point. Does nothing if there is no
* link at this point.
* @param point the point.
*/
public void link(Point point) {
BytecodeLink link = getLink(point);
if (link == null) {
return;
}
updateHistory(link.sourceOffset);
if (link instanceof ConstantPoolLink) {
ConstantPoolHyperlinkListener.link(detailPane.getBrowserServices(), ((ConstantPoolLink)link).cpIndex);
} else if (link instanceof OffsetLink) {
int targetOffset = ((OffsetLink)link).targetOffset;
scrollToOffset(targetOffset);
updateHistory(targetOffset);
}
}
/**
* Returns whether there is a link below the given point.
* @param point the point.
* @return the value.
*/
public boolean isLink(Point point) {
return getLink(point) != null;
}
/**
* Scroll the view to a given bytecode offset.
* @param offset the bytecode offset.
*/
public void scrollToOffset(int offset) {
Integer line = offsetToLine.get(new Integer(offset));
if (line == null) {
return;
}
Rectangle target = new Rectangle(0, line * lineHeight + MARGIN_Y + 1, 10, getParent().getHeight());
scrollRectToVisible(target);
}
/**
* Copy the view text to the clipboard.
*/
public void copyToClipboard() {
StringBuilder buffer = new StringBuilder();
for (String line : textLines) {
buffer.append(line);
buffer.append('\n');
}
StringSelection stringSelection = new StringSelection(buffer.toString());
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, stringSelection);
}
protected void paintComponent(Graphics graphics) {
if (lineHeight == 0) {
return;
}
Graphics2D g = (Graphics2D)graphics;
g.translate(MARGIN_X, MARGIN_Y);
Rectangle clipBounds = graphics.getClipBounds();
Paint oldPaint = g.getPaint();
g.setPaint(Color.WHITE);
g.fill(clipBounds);
g.setPaint(oldPaint);
int startLine = Math.max(0, clipBounds.y / lineHeight - 1);
int endLine = Math.min(lines.size(), (clipBounds.y + clipBounds.height) / lineHeight + 1);
for (int i = startLine; i < endLine; i++) {
TextLayout textLayout = getOrCreateTextLayout(i);
textLayout.draw(g, 0, i * lineHeight + textLayout.getAscent());
}
g.translate(-MARGIN_X, -MARGIN_Y);
}
private TextLayout getOrCreateTextLayout(int i) {
TextLayout textLayout = textLayouts[i];
if (textLayout == null) {
textLayout = textLayouts[i] = new TextLayout((lines.get(i)).getIterator(), frc);
}
return textLayout;
}
private void setupComponent() {
setBorder(BORDER);
setDoubleBuffered(false);
setOpaque(false);
}
private void setupEventHandlers() {
}
private BytecodeLink getLink(Point point) {
if (lineHeight == 0) {
return null;
}
int x = point.x - MARGIN_X;
int y = point.y - MARGIN_Y;
int line = y / lineHeight;
BytecodeLink link = lineToLink.get(line);
if (link == null) {
return null;
}
TextLayout textLayout = getOrCreateTextLayout(line);
TextHitInfo textHitInfo = textLayout.hitTestChar(x, y - line * lineHeight);
int charIndex = textHitInfo.getCharIndex();
if (charIndex >= link.startCharIndex && charIndex < link.endCharIndex) {
return link;
} else {
return null;
}
}
private void updateHistory(int offset) {
BrowserServices services = detailPane.getBrowserServices();
TreePath treePath = services.getBrowserComponent().getTreePane().getTree().getSelectionPath();
BrowserHistory history = services.getBrowserComponent().getHistory();
history.updateHistory(treePath, offset);
}
private void setupTextLayouts() {
lineHeight = 0;
currentHeight = 0f;
currentWidth = 0f;
textLines.clear();
lines.clear();
textLayouts = null;
offsetToLine.clear();
lineToLink.clear();
invalidBranches.clear();
byte[] code = codeAttribute.getCode();
try {
ArrayList<AbstractInstruction> instructions = ByteCodeReader.readByteCode(code);
verifyOffsets(instructions);
calculateOffsetWidth(instructions);
detailPane.setCurrentInstructions(instructions);
for (AbstractInstruction instruction : instructions) {
addInstructionToDocument(instruction);
}
textLayouts = new TextLayout[lines.size()];
} catch (IOException ex) {
ex.printStackTrace();
}
setPreferredSize(new Dimension((int)currentWidth + 2 * MARGIN_X, (int)currentHeight + 2 * MARGIN_Y));
}
private void verifyOffsets(ArrayList<AbstractInstruction> instructions) {
int instructionsLength = instructions.size();
for (int i = 0; i < instructionsLength; i++) {
AbstractInstruction instruction = instructions.get(i);
if (instruction instanceof AbstractBranchInstruction) {
int branchOffset = ((AbstractBranchInstruction)instruction).getBranchOffset();
int targetDistance = 0;
if (branchOffset > 0) {
branchOffset -= instruction.getSize();
while (branchOffset > 0 && i + targetDistance + 1 < instructionsLength) {
++targetDistance;
branchOffset -= (instructions.get(i + targetDistance)).getSize();
}
} else {
while (branchOffset < 0 && i + targetDistance > 0) {
--targetDistance;
branchOffset += (instructions.get(i + targetDistance)).getSize();
}
}
if (branchOffset != 0) {
invalidBranches.add(instruction);
}
}
}
}
private void calculateOffsetWidth(java.util.List<AbstractInstruction> instructions) {
int numberOfInstructions = instructions.size();
if (numberOfInstructions > 0) {
AbstractInstruction lastInstruction = instructions.get(numberOfInstructions - 1);
offsetWidth = String.valueOf(lastInstruction.getOffset()).length();
} else {
offsetWidth = 1;
}
StringBuilder buffer = new StringBuilder(offsetWidth);
for (int i = 0; i < offsetWidth; i++) {
buffer.append(' ');
}
offsetBlank = buffer.toString();
}
private void addInstructionToDocument(AbstractInstruction instruction) {
int offset = instruction.getOffset();
addOffsetReference(offset);
appendString(getPaddedValue(offset, offsetWidth),
STYLE_OFFSET);
appendString(" " + instruction.getOpcode().getVerbose(),
STYLE_INSTRUCTION);
addOpcodeSpecificInfo(instruction);
newLine();
}
private void addOffsetReference(int offset) {
offsetToLine.put(offset, getCurrentLine());
}
private void addOpcodeSpecificInfo(AbstractInstruction instruction) {
if (instruction instanceof ImmediateByteInstruction) {
addImmediateByteSpecificInfo((ImmediateByteInstruction)instruction);
} else if (instruction instanceof ImmediateShortInstruction) {
addImmediateShortSpecificInfo((ImmediateShortInstruction)instruction);
} else if (instruction instanceof AbstractBranchInstruction) {
addBranchSpecificInfo((AbstractBranchInstruction)instruction);
} else if (instruction instanceof TableSwitchInstruction) {
addTableSwitchSpecificInfo((TableSwitchInstruction)instruction);
} else if (instruction instanceof LookupSwitchInstruction) {
addLookupSwitchSpecificInfo((LookupSwitchInstruction)instruction);
}
}
private void addImmediateByteSpecificInfo(ImmediateByteInstruction instruction) {
Opcode opcode = instruction.getOpcode();
int sourceOffset = instruction.getOffset();
int immediateByte = instruction.getImmediateByte();
if (opcode == Opcode.LDC) {
addConstantPoolLink(immediateByte, sourceOffset);
} else if (opcode == Opcode.NEWARRAY) {
NewArrayType newArrayType = NewArrayType.getFromCode(immediateByte);
String verbose = newArrayType == null ? "invalid array type" : newArrayType.getVerbose();
appendString(" " + immediateByte + " (" + verbose + ")",
STYLE_IMMEDIATE_VALUE);
} else if (opcode == Opcode.BIPUSH) {
appendString(" " + (byte)immediateByte, STYLE_IMMEDIATE_VALUE);
} else {
appendString(" " + immediateByte,
STYLE_IMMEDIATE_VALUE);
if (instruction instanceof IncrementInstruction) {
appendString(" by", STYLE_NORMAL);
appendString(" " + ((IncrementInstruction)instruction).getIncrementConst(),
STYLE_IMMEDIATE_VALUE);
}
}
}
private void addImmediateShortSpecificInfo(ImmediateShortInstruction instruction) {
Opcode opcode = instruction.getOpcode();
int sourceOffset = instruction.getOffset();
int immediateShort = instruction.getImmediateShort();
if (opcode == Opcode.SIPUSH) {
appendString(" " + immediateShort,
STYLE_IMMEDIATE_VALUE);
} else {
addConstantPoolLink(immediateShort, sourceOffset);
if (instruction instanceof InvokeInterfaceInstruction) {
appendString(" count " + ((InvokeInterfaceInstruction)instruction).getCount(),
STYLE_IMMEDIATE_VALUE);
} else if (instruction instanceof MultianewarrayInstruction) {
appendString(" dim " + ((MultianewarrayInstruction)instruction).getDimensions(),
STYLE_IMMEDIATE_VALUE);
}
}
}
private void addBranchSpecificInfo(AbstractBranchInstruction instruction) {
int branchOffset = instruction.getBranchOffset();
int instructionOffset = instruction.getOffset();
addOffsetLink(branchOffset, instructionOffset);
if (invalidBranches.contains(instruction)) {
appendString(" [INVALID BRANCH]", STYLE_NORMAL);
}
}
private void addTableSwitchSpecificInfo(TableSwitchInstruction instruction) {
int instructionOffset = instruction.getOffset();
int lowByte = instruction.getLowByte();
int highByte = instruction.getHighByte();
int[] jumpOffsets = instruction.getJumpOffsets();
appendString(" " + lowByte + " to " + highByte, STYLE_IMMEDIATE_VALUE);
newLine();
for (int i = 0; i <= highByte - lowByte; i++) {
appendString(offsetBlank + TAB_STRING + (i + lowByte) + ": ", STYLE_IMMEDIATE_VALUE);
addOffsetLink(jumpOffsets[i], instructionOffset);
newLine();
}
appendString(offsetBlank + TAB_STRING + "default: ", STYLE_IMMEDIATE_VALUE);
addOffsetLink(instruction.getDefaultOffset(), instructionOffset);
}
private void addLookupSwitchSpecificInfo(LookupSwitchInstruction instruction) {
int instructionOffset = instruction.getOffset();
List matchOffsetPairs = instruction.getMatchOffsetPairs();
int matchOffsetPairsCount = matchOffsetPairs.size();
appendString(" " + matchOffsetPairsCount, STYLE_IMMEDIATE_VALUE);
newLine();
MatchOffsetPair matchOffsetPairEntry;
for (Object matchOffsetPair : matchOffsetPairs) {
matchOffsetPairEntry = (MatchOffsetPair)matchOffsetPair;
appendString(offsetBlank + TAB_STRING + matchOffsetPairEntry.getMatch() + ": ",
STYLE_IMMEDIATE_VALUE);
addOffsetLink(matchOffsetPairEntry.getOffset(), instructionOffset);
newLine();
}
appendString(offsetBlank + TAB_STRING + "default: ", STYLE_IMMEDIATE_VALUE);
addOffsetLink(instruction.getDefaultOffset(), instructionOffset);
}
private void addConstantPoolLink(int constantPoolIndex, int sourceOffset) {
appendString(" ", STYLE_NORMAL);
int startCharIndex = getCurrentCharIndex();
appendString("#" + constantPoolIndex, STYLE_LINK);
int endCharIndex = getCurrentCharIndex();
lineToLink.put(getCurrentLine(), new ConstantPoolLink(startCharIndex, endCharIndex, sourceOffset, constantPoolIndex));
try {
String name = classFile.getConstantPoolEntryName(constantPoolIndex);
if (name != null) {
if (name.length() > 0) {
appendString(" <" + name + ">", STYLE_SMALL);
}
} else {
appendString(" [INVALID]", STYLE_SMALL);
}
} catch (InvalidByteCodeException ex) {
}
}
private void addOffsetLink(int branchOffset, int sourceOffset) {
int targetOffset = branchOffset + sourceOffset;
appendString(" ", STYLE_NORMAL);
int startCharIndex = getCurrentCharIndex();
appendString(String.valueOf(targetOffset), STYLE_LINK);
int endCharIndex = getCurrentCharIndex();
lineToLink.put(getCurrentLine(), new OffsetLink(startCharIndex, endCharIndex, sourceOffset, targetOffset));
appendString(" (" + (branchOffset > 0 ? "+" : "") + String.valueOf(branchOffset) + ")",
STYLE_IMMEDIATE_VALUE);
}
private int getCurrentCharIndex() {
int offset = 0;
for (LineCacheEntry entry : currentLineCache) {
offset += entry.text.length();
}
return offset;
}
private int getCurrentLine() {
return lines.size();
}
private void appendString(String text, Map<TextAttribute, Object> attributes) {
currentLineCache.add(new LineCacheEntry(text, attributes));
}
private void newLine() {
String text = getCurrentLineText();
AttributedString attrString = new AttributedString(text, STYLE_BASE);
int startCharIndex = 0;
for (LineCacheEntry entry : currentLineCache) {
int endCharIndex = startCharIndex + entry.text.length();
attrString.addAttributes(entry.attributes, startCharIndex, endCharIndex);
startCharIndex = endCharIndex;
}
lines.add(attrString);
textLines.add(text);
if (lineHeight == 0) {
TextLayout textLayout = new TextLayout(attrString.getIterator(), frc);
lineHeight = (int)(textLayout.getAscent() + textLayout.getDescent() + textLayout.getLeading());
ascent = (int)textLayout.getAscent();
textLayout = new TextLayout("0", STYLE_BASE, frc);
characterWidth = (int)textLayout.getAdvance();
}
currentHeight += lineHeight;
currentWidth = Math.max(currentWidth, characterWidth * text.length());
currentLineCache.clear();
}
private String getCurrentLineText() {
StringBuilder buffer = new StringBuilder(getCurrentLineLength());
for (LineCacheEntry entry : currentLineCache) {
buffer.append(entry.text);
}
return buffer.toString();
}
private int getCurrentLineLength() {
int length = 0;
for (LineCacheEntry entry : currentLineCache) {
length += entry.text.length();
}
return length;
}
private static class LineCacheEntry {
private String text;
private Map<TextAttribute, Object> attributes;
private LineCacheEntry(String text, Map<TextAttribute, Object> attributes) {
this.text = text;
this.attributes = attributes;
}
}
private static class BytecodeLink {
private int startCharIndex;
private int endCharIndex;
protected int sourceOffset;
private BytecodeLink(int startCharIndex, int endCharIndex, int sourceOffset) {
this.startCharIndex = startCharIndex;
this.endCharIndex = endCharIndex;
this.sourceOffset = sourceOffset;
}
}
private static class ConstantPoolLink extends BytecodeLink {
private int cpIndex;
private ConstantPoolLink(int startCharIndex, int endCharIndex, int sourceOffset, int cpIndex) {
super(startCharIndex, endCharIndex, sourceOffset);
this.cpIndex = cpIndex;
}
}
private static class OffsetLink extends BytecodeLink {
private int targetOffset;
private OffsetLink(int startCharIndex, int endCharIndex, int sourceOffset, int targetOffset) {
super(startCharIndex, endCharIndex, sourceOffset);
this.targetOffset = targetOffset;
}
}
}