/**
* Copyright (C) 2014 Eric Van Dewoestine
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.eclim.plugin.jdt.command.debug.ui;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclim.logging.Logger;
import org.eclim.plugin.jdt.command.debug.context.ThreadContext;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.debug.core.IJavaArray;
import org.eclipse.jdt.debug.core.IJavaFieldVariable;
import org.eclipse.jdt.debug.core.IJavaObject;
import org.eclipse.jdt.debug.core.IJavaReferenceType;
import org.eclipse.jdt.debug.core.IJavaThread;
import org.eclipse.jdt.debug.core.IJavaType;
import org.eclipse.jdt.debug.core.IJavaValue;
import org.eclipse.jdt.debug.core.IJavaVariable;
import org.eclipse.jdt.internal.debug.core.logicalstructures.JDIAllInstancesValue;
import org.eclipse.jdt.internal.debug.core.model.JDIReferenceListValue;
import org.eclipse.jdt.internal.debug.core.model.JDIValue;
import org.eclipse.jdt.internal.debug.core.model.JDIVariable;
import org.eclipse.osgi.util.NLS;
/**
* UI model for displaying variables.
*
* <p>
* The formatting code is borrowed from Eclipse JDT UI.
* @see org.eclipse.jdt.internal.debug.ui.JDIModelPresentation
*/
public class VariableView
{
private static final Logger logger =
Logger.getLogger(VariableView.class);
/**
* Depth of the root node.
*/
private static final int ROOT_DEPTH = 0;
/**
* The selector of <code>java.lang.Object#toString()</code>,
* used to evaluate 'toString()' for displaying details of a value.
*/
private final String toStringSelector = "toString";
/**
* The signature of <code>java.lang.Object#toString()</code>,
* used to evaluate 'toString()' for displaying details of a value.
*/
private final String toStringSig = "()Ljava/lang/String;";
/**
* Thread being shown in UI.
*/
private IJavaThread viewingThread;
/**
* Expanded variable map for the thread being shown in UI.
*/
private Map<Long, ExpandableVar> expandableVarMap =
new HashMap<Long, ExpandableVar>();
private ThreadContext threadCtx;
/**
* Variable value that is shown in UI and is expandable; i.e., has inner
* variables/fields. These values are instances of IJavaObject.
*/
private class ExpandableVar
{
private IJavaValue value;
/**
* Determines if the variable value is expanded in the UI.
*/
private boolean expanded = false;
/**
* Depth of this variable in tree. The root node will have depth = 0.
*/
private int depth;
public ExpandableVar(IJavaValue value, int depth)
{
this.value = value;
this.depth = depth;
}
}
public VariableView(ThreadContext threadCtx)
{
this.threadCtx = threadCtx;
}
/**
* Returns the variable view for the thread currently being stepped through.
* If there is no such thread, then a <code>null</code> is returned.
*/
public List<String> get()
throws DebugException
{
IJavaThread thread = threadCtx.getSteppingThread();
// Since the view is being reloaded, we can clear existing entries
clear();
if (thread == null) {
return null;
} else {
this.viewingThread = thread;
List<String> results = new ArrayList<String>();
// Protect against variable information unavailable for native
// methods
try {
IStackFrame stackFrame = thread.getTopStackFrame();
if (stackFrame != null) {
process(thread.getTopStackFrame().getVariables(), results, ROOT_DEPTH);
}
} catch (DebugException e) {
// Suppress exception as it is possible to get an error when the current
// stack frame points to native method. Variable information is not
// available in this case.
if (logger.isDebugEnabled()) {
logger.debug("Unable to get variables", e);
}
}
return results;
}
}
public List<String> expandValue(long valueId)
{
ExpandableVar expandableVar = expandableVarMap.get(valueId);
if (expandableVar == null) {
if (logger.isDebugEnabled()) {
logger.debug("No variable value found with ID: " + valueId);
}
return null;
}
// If variable is already expanded, just return.
if (expandableVar.expanded) {
return null;
}
IJavaValue value = expandableVar.value;
List<String> results = new ArrayList<String>();
// Suppress any exception. No point in letting it propagate
try {
process(value.getVariables(), results, expandableVar.depth + 1);
// Mark as expanded.
expandableVar.expanded = true;
} catch (DebugException e) {
logger.error("Unable to get variables", e);
}
return results;
}
public void clear()
throws DebugException
{
viewingThread = null;
expandableVarMap.clear();
}
public boolean isViewingThread(IJavaThread thread)
throws DebugException
{
return viewingThread != null &&
(thread.getThreadObject().getUniqueId() ==
viewingThread.getThreadObject().getUniqueId());
}
/**
* Process the variables and adds them to the result set.
* Some variables may be excluded because they are not important for
* deugging purposes.
* @see #ignoreVar method.
*
* @param vars variables
* @param results final results containing the variable text
* @param depth current nesting depth in the tree hierarchy
*/
private void process(IVariable[] vars, List<String> results, int depth)
throws DebugException
{
if (vars == null) {
return;
}
for (IVariable var : vars) {
JDIVariable jvar = (JDIVariable) var;
if (jvar.isSynthetic() ||
ignoreVar(jvar))
{
continue;
}
JDIValue value = (JDIValue) var.getValue();
boolean isLeafNode = !((value != null) &&
(value instanceof IJavaObject) &&
value.hasVariables());
// Treat String as leaf node even though it has child variables
isLeafNode = isLeafNode || ViewUtils.isStringValue(value);
String prefix = getIndentation(depth, isLeafNode);
results.add(prefix + getVariableText(jvar));
// Keep track of this value as it is shown in UI and could be expanded
if (!isLeafNode) {
long valueId = ((IJavaObject) value).getUniqueId();
// If this value was already seen, then don't update the map. This is
// to prevent infinite recursion and also to not change the node depth
// of the previously seen value. This case is normal for enum.
if (!expandableVarMap.containsKey(valueId)) {
expandableVarMap.put(valueId,
new ExpandableVar(value, depth));
}
// Hack: Add an empty line so that VIM will think there are child nodes
// and fold correctly.
String childPrefix = getIndentation(depth + 1, true);
results.add(childPrefix);
}
}
}
/**
* Returns the toString value of the Java object.
*/
public String getDetail(long valueId)
throws DebugException
{
ExpandableVar expandableVar = expandableVarMap.get(valueId);
if (expandableVar == null) {
if (logger.isDebugEnabled()) {
logger.debug("No variable value found with ID: " + valueId);
}
return ViewUtils.UNKNOWN;
}
IJavaValue value = expandableVar.value;
if (value instanceof IJavaObject) {
IJavaValue toStrValue = ((IJavaObject) value).sendMessage(
toStringSelector,
toStringSig,
null,
viewingThread,
false);
return toStrValue == null ? ViewUtils.UNKNOWN : toStrValue.getValueString();
} else {
return ViewUtils.UNKNOWN;
}
}
/**
* Igmores final primitive variables.
*/
private boolean ignoreVar(JDIVariable var)
throws DebugException
{
if (var.isFinal()) {
JDIValue value = (JDIValue) var.getValue();
if (value instanceof IJavaObject) {
return false;
} else {
return true;
}
}
return false;
}
/**
* Returns the prefix string to use to simulate indentation.
*/
private String getIndentation(int level, boolean isLeafNode)
{
if (level == ROOT_DEPTH) {
if (isLeafNode) {
return ViewUtils.LEAF_NODE_SYMBOL;
} else {
return ViewUtils.EXPANDED_NODE_SYMBOL;
}
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < level - 1; i++) {
sb.append(ViewUtils.LEAF_NODE_INDENT);
}
// Special indent for last level since the tree symbol is involved
if (isLeafNode) {
sb.append(ViewUtils.LEAF_NODE_INDENT +
ViewUtils.LEAF_NODE_SYMBOL);
} else {
sb.append(ViewUtils.NON_LEAF_NODE_INDENT +
ViewUtils.EXPANDED_NODE_SYMBOL);
}
return sb.toString();
}
private String getVariableText(IJavaVariable var)
{
String varLabel = ViewUtils.UNKNOWN;
try {
varLabel = var.getName();
} catch (DebugException exception) {}
IJavaValue javaValue = null;
try {
javaValue = (IJavaValue) var.getValue();
} catch (DebugException e1) {}
StringBuilder buff = new StringBuilder();
buff.append(varLabel);
// Add declaring type name if required
if (var instanceof IJavaFieldVariable) {
IJavaFieldVariable field = (IJavaFieldVariable) var;
if (isDuplicateName(field)) {
try {
String decl = field.getDeclaringType().getName();
buff.append(NLS.bind(" ({0})",
new String[]{ViewUtils.getQualifiedName(decl)}));
} catch (DebugException e) {}
}
}
String valueString = getFormattedValueText(javaValue);
// Do not put the equal sign for array partitions
if (valueString.length() != 0) {
buff.append(" = ");
buff.append(valueString);
}
return buff.toString();
}
/**
* Returns whether the given field variable has the same name as any variables
*/
private boolean isDuplicateName(IJavaFieldVariable variable)
{
IJavaReferenceType javaType = variable.getReceivingType();
try {
String[] names = javaType.getAllFieldNames();
boolean found = false;
for (int i = 0; i < names.length; i++) {
if (variable.getName().equals(names[i])) {
if (found) {
return true;
}
found = true;
}
}
return false;
} catch (DebugException e) {}
return false;
}
/**
* Returns text for the given value based on user preferences to display
* toString() details.
*
* @param javaValue
* @return text
*/
private String getFormattedValueText(IJavaValue javaValue)
{
String valueString = ViewUtils.UNKNOWN;
if (javaValue != null) {
try {
valueString = getValueText(javaValue);
} catch (DebugException exception) {}
}
return valueString;
}
/**
* Build the text for an IJavaValue.
*
* @param value the value to get the text for
* @return the value string
* @throws DebugException if something happens trying to compute the value string
*/
private String getValueText(IJavaValue value)
throws DebugException
{
String refTypeName = value.getReferenceTypeName();
String valueString = value.getValueString();
boolean isString = ViewUtils.isStringValue(value);
IJavaType type = value.getJavaType();
String signature = null;
if (type != null) {
signature = type.getSignature();
}
if ("V".equals(signature)) {
valueString = ViewUtils.NO_EXPLICIT_RETURN_VALUE;
}
boolean isObject = isObjectValue(signature);
boolean isArray = value instanceof IJavaArray;
StringBuilder buffer = new StringBuilder();
if (isUnknown(signature)) {
buffer.append(signature);
} else if (isObject && !isString && (refTypeName.length() > 0)) {
// Don't show type name for instances and references
if (!(value instanceof JDIReferenceListValue ||
value instanceof JDIAllInstancesValue))
{
String qualTypeName = ViewUtils.getQualifiedName(refTypeName).trim();
if (isArray) {
qualTypeName = adjustTypeNameForArrayIndex(qualTypeName,
((IJavaArray)value).getLength());
}
buffer.append(qualTypeName);
buffer.append(' ');
}
}
// Put double quotes around Strings
if (valueString != null && (isString || valueString.length() > 0)) {
if (isString) {
buffer.append('"');
}
buffer.append(valueString);
if (isString) {
buffer.append('"');
}
}
return buffer.toString().trim();
}
/**
* Given a JNI-style signature String for a IJavaValue, return true
* if the signature represents an Object or an array of Objects.
*
* @param signature the signature to check
* @return <code>true</code> if the signature represents an object;
* <code>false</code> otherwise
*/
private boolean isObjectValue(String signature)
{
if (signature == null) {
return false;
}
String type = Signature.getElementType(signature);
char sigchar = type.charAt(0);
if(sigchar == Signature.C_UNRESOLVED ||
sigchar == Signature.C_RESOLVED)
{
return true;
}
return false;
}
/**
* Returns <code>true</code> if the given signature is not <code>null</code> and
* matches the text '<unknown>'
*
* @param signature the signature to compare
* @return <code>true</code> if the signature matches '<unknown>'
*/
boolean isUnknown(String signature)
{
if(signature == null) {
return false;
}
return ViewUtils.UNKNOWN.equals(signature);
}
/**
* Given the reference type name of an array type, insert the array length
* in between the '[]' for the first dimension and return the result.
*/
private String adjustTypeNameForArrayIndex(String typeName, int arrayIndex)
{
int firstBracket = typeName.indexOf("[]");
if (firstBracket < 0) {
return typeName;
}
StringBuilder buffer = new StringBuilder(typeName);
buffer.insert(firstBracket + 1, Integer.toString(arrayIndex));
return buffer.toString();
}
}