/*******************************************************************************
* Copyright (c) 2013 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package org.jboss.tools.common.java.impl;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMemberValuePair;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.compiler.env.ISourceField;
import org.eclipse.jdt.internal.core.JavaElement;
import org.jboss.tools.common.core.CommonCorePlugin;
import org.jboss.tools.common.util.EclipseJavaUtil;
import org.jboss.tools.common.util.StringUtil;
/**
* IField.getConstant() and IMemberValuePair.getValue() return non-null object
* only for trivial values, even a simple expression 2 + 2 results in null.
*
* There is a way out, to build AST expression and resolve it, but that requires
* building AST tree for the entire compilation unit, so that if a client model
* avoids AST for the sake of performance, it is unacceptable.
*
* This class provides resolution of primitive type expressions.
*
* Supported:
* - references to static final fields - MyType.MY_CONSTANT;
* - numerics, strings and characters - 1 2L 2.0 3.14D "a string" 'c';
* - conversion between primitive types - (short)3L;
* - arithmetic operations, parenthesis - (2 + 3) * (MyType.MY_CONSTANT + 1) / (2 - 1);
* Not supported:
* - method and constructor calls.
*
* @author Viacheslav Kabanovich
*
*/
public class ValueResolver {
private IJavaElement element;
private Object constant = null;
private ReferenceResolver referenceResolver = null;
public ValueResolver(IJavaElement element) {
this.element = element;
}
/**
* Constant is computed at each call to resolvePair(IMemberValuePair).
* It is to be requested before resolving next pair.
* @return
*/
public Object getConstant() {
return constant;
}
/**
* Call this method after having used to resolve all expressions
* and/or members. When resolving a reference to a constant,
* the implementation may modify compilation unit copy to get
* access to type resolution. This copy must be discarded.
*/
public void dispose() {
if(referenceResolver != null) {
referenceResolver.dispose();
referenceResolver = null;
}
}
/**
* For a complex expression returns source string if it is
* available while the result of calculation is to be requested
* by getConstant().
*
* For a reference to a constant returns resolved qualified name
* while the constant value is to be requested by getConstant().
*
* Otherwise, returns pair.getValue(). If the value is an array,
* then for each element that is a reference to a constant
* that element is replaced with resolved qualified name
* of the reference.
*
* @param pair
* @return
*/
public Object resolvePair(IMemberValuePair pair) {
constant = null;
Object value = pair.getValue();
int k = pair.getValueKind();
if(k == IMemberValuePair.K_QUALIFIED_NAME || k == IMemberValuePair.K_SIMPLE_NAME
|| (value instanceof Object[] && k == IMemberValuePair.K_UNKNOWN)) {
if(element != null && element.getAncestor(IJavaElement.COMPILATION_UNIT) instanceof ICompilationUnit) {
value = resolve(value);
}
} else if(k == IMemberValuePair.K_UNKNOWN && value == null) {
if(element instanceof ISourceReference) {
try {
String source = getExpressionForName(pair.getMemberName());
if(source != null) {
Object c = resolveExpression(source);
if(c != null) {
value = source;
constant = c;
}
}
} catch (CoreException e) {
CommonCorePlugin.getDefault().logError(e);
}
}
}
return value;
}
private String getExpressionForName(String name) throws CoreException {
if(name == null) {
name = AnnotationDeclaration.VALUE;
}
String source = getExpression();
if(source != null) {
if(source.indexOf('=') < 0) {
if(AnnotationDeclaration.VALUE.equals(name)) {
return source;
} else {
return null;
}
}
StringTokenizer st = new StringTokenizer(source, ",");
while(st.hasMoreTokens()) {
String t = st.nextToken().trim();
int i = t.indexOf('=');
if(i < 0) continue;
if(t.substring(0, i).trim().equals(name)) {
return t.substring(i + 1).trim();
}
}
}
return null;
}
/**
* Simple resolve when JDT resolved value and knows value type.
* @param value
* @return
*/
private Object resolve(Object value) {
if(value instanceof Object[]) {
Object[] vs = (Object[])value;
for (int i = 0; i < vs.length; i++) {
vs[i] = resolve(vs[i]);
}
constant = null; // getConstant() would return not array but one of its values.
} else if (value != null && isNameToken(value.toString())) {
try {
if(connect()) {
value = referenceResolver.resolveReference(value);
}
} catch (CoreException e) {
CommonCorePlugin.getDefault().logError(e);
}
}
return value;
}
private String getExpression() throws CoreException {
String source = ((ISourceReference)element).getSource();
if(source != null) {
int b = source.indexOf('(');
int e = source.lastIndexOf(')');
if(b > 0 && e > b) {
return source.substring(b + 1, e).trim();
}
}
return null;
}
private void setFieldInitialValueToConstant(IField f) throws JavaModelException {
Object c = getFieldInitialValue(f);
if(c != null) {
if(c instanceof String) {
constant = StringUtil.trimQuotes(c.toString());
} else if(c instanceof Number || c instanceof Boolean) {
constant = c;
} else {
constant = c.toString();
}
}
}
/**
* Returns calculated initial value of field.
* @param f
* @return
* @throws JavaModelException
*/
public static Object getFieldInitialValue(IField f) throws JavaModelException {
Object c = f.getConstant();
if(c == null && (((JavaElement)f).getElementInfo() instanceof ISourceField)) {
char[] cs = ((ISourceField)((JavaElement)f).getElementInfo()).getInitializationSource();
if(cs != null) {
ValueResolver r = new ValueResolver(f);
c = r.resolveExpression(new String(cs));
r.dispose();
}
}
return c;
}
public Object resolveExpression(String expression) {
Expression expr = new Expression(expression, 0, expression.length());
try {
return expr.compute();
} catch (WrongExpressionException exc) {
//ignore - user input
}
return null;
}
private boolean connect() throws CoreException {
if(referenceResolver == null) {
referenceResolver = new ReferenceResolver();
}
return referenceResolver.connect();
}
static class WrongExpressionException extends Exception {
public WrongExpressionException(String message) {
super(message);
}
}
class Expression {
String expression;
int from;
int to;
int index;
Object result = null;
public Expression(String expression, int from, int to) {
this.expression = expression;
this.from = from;
this.to = to;
index = from;
skipSpaces();
}
void skipSpaces() {
while(index < to && Character.isWhitespace(expression.charAt(index))) {
index++;
}
}
int getOperandTokenEnd() {
if(index == to) {
return index;
}
for (int i = index; i < to; i++) {
char ch = expression.charAt(i);
if(!Character.isJavaIdentifierPart(ch) && ch != '.') {
return i;
}
}
return to;
}
public Object compute() throws WrongExpressionException {
Object left = computeOperand();
skipSpaces();
if(index == to) {
result = left;
} else {
char ch = expression.charAt(index);
while(ch == '*' || ch == '/') {
index++;
Object right = computeOperand();
skipSpaces();
if(ch == '*') {
left = multiply(left, right);
} else if(ch == '/') {
left = divide(left, right);
}
if(index == to) {
result = left;
return result;
} else {
ch = expression.charAt(index);
}
}
if(ch == '+') {
index++;
Object right = compute();
return add(left, 0, right);
} else if(ch == '-') {
Object right = compute();
return add(left, 0, right);
}
}
return result;
}
Object computeOperand() throws WrongExpressionException {
skipSpaces();
if(index == to) {
throw new WrongExpressionException("Operand expected");
}
char ch = expression.charAt(index);
if(ch == '"') {
int m = findMatchingQuote(expression, index, to);
if(m < 0) throw new WrongExpressionException("Quote does not match.");
int b = index + 1;
index = m;
return expression.substring(b, m - 1);
} else if(ch == '\'') {
int m = findMatchingQuote(expression, index, to);
if(m < 0) throw new WrongExpressionException("Quote does not match.");
int b = index + 1;
index = m;
String v = expression.substring(b, m - 1);
if(v.length() == 1) {
return new Character(expression.charAt(b));
} else if(v.startsWith("\\")) {
if(v.equals("\\n")) return new Character('\n');
if(v.equals("\\r")) return new Character('\r');
if(v.equals("\\t")) return new Character('\t');
if(v.equals("\\b")) return new Character('\b');
if(v.equals("\\f")) return new Character('\f');
if(v.equals("\\'")) return new Character('\'');
if(v.equals("\\\"")) return new Character('"');
if(v.equals("\\\\")) return new Character('\\');
if(v.startsWith("\\u") && v.length() == 6) {
try {
return new Character((char)Integer.parseInt(v.substring(2), 16));
} catch (NumberFormatException e) {
//ignore - user input
}
}
}
throw new WrongExpressionException("Not supported character " + v);
} else if(ch == '(') {
int m = findMatchingBrace(expression, index, to);
if(m < 0) throw new WrongExpressionException("Braces does not match.");
String sub = expression.substring(index + 1, m - 1).trim();
if(sub.length() == 0) throw new WrongExpressionException("Expression expected at " + (index + 1));
if(PRIMITIVE_TYPES.contains(sub)) {
index = m;
Object o = computeOperand();
if(o instanceof Character) {
if(!"char".equals(sub)) {
o = new Integer((int)((Character)o).charValue());
} else {
return o;
}
}
if(o instanceof Number) {
if("int".equals(sub)) {
return new Integer(((Number)o).intValue());
} else if("short".equals(sub)) {
return new Short(((Number)o).shortValue());
} else if("byte".equals(sub)) {
return new Byte(((Number)o).byteValue());
} else if("long".equals(sub)) {
return new Long(((Number)o).longValue());
} else if("float".equals(sub)) {
return new Float(((Number)o).floatValue());
} else if("double".equals(sub)) {
return new Double(((Number)o).doubleValue());
} else if("char".equals(sub)) {
return new Character((char)((Number)o).intValue());
}
} else {
throw new WrongExpressionException("Cannot convert to " + sub + ".");
}
} else {
Expression subExpression = new Expression(expression, index + 1, m - 1);
Object o = subExpression.compute();
index = m;
skipSpaces();
return o;
}
} else if(ch == '+') {
index++;
return computeOperand();
} else if(ch == '-') {
index++;
Object o = computeOperand();
if(o instanceof Number) {
Number n = (Number)o;
if(n instanceof Long) return new Long(-n.longValue());
if(n instanceof Float) return new Float(-n.floatValue());
if(n instanceof Double) return new Double(-n.doubleValue());
return new Integer(-n.intValue());
} else if(o instanceof Character) {
if(o instanceof Character) return new Integer(-((Character)o).charValue());
} else {
throw new WrongExpressionException("Cannot compute negative of non-number");
}
} else {
int e = getOperandTokenEnd();
if(e == index) {
throw new WrongExpressionException("Operand expected at " + index);
}
String t = expression.substring(index, e);
index = e;
if(isNameToken(t)) {
try {
constant = null;
Object o = connect() ? referenceResolver.resolveReference(t) : null;
if(constant != null) {
return constant;
} else if (o != null) {
return o;
}
throw new WrongExpressionException("Cannut resolve name " + t);
} catch (CoreException exc) {
CommonCorePlugin.getDefault().logError(exc);
}
} else {
try {
return Integer.parseInt(t);
} catch (NumberFormatException exc) {
//ignore - user input
}
try {
if(t.toLowerCase().endsWith("l")) {
t = t.substring(0, t.length() - 1);
}
return Long.parseLong(t);
} catch (NumberFormatException exc) {
//ignore - user input
}
try {
if(t.toLowerCase().endsWith("d") || t.toLowerCase().endsWith("f")) {
t = t.substring(0, t.length() - 1);
}
return Double.parseDouble(t);
} catch (NumberFormatException exc) {
//ignore - user input
}
}
throw new WrongExpressionException("Cannut resolve value " + t);
}
return null;
}
}
class ReferenceResolver {
private boolean isConnected = false;
private ICompilationUnit u = null;
private ICompilationUnit u2 = null;
private IType type = null;
ReferenceResolver() {}
private boolean connect() throws CoreException {
if(isConnected) {
return type != null;
}
isConnected = true;
if(element == null) {
return false;
}
u = (ICompilationUnit)element.getAncestor(IJavaElement.COMPILATION_UNIT);
if(u == null) {
return false;
}
u2 = null;
type = (IType)element.getAncestor(IJavaElement.TYPE);
if(type == null) {
if(u != null && element.getParent() instanceof IPackageDeclaration) {
IType[] ts = u.getTypes();
if(ts != null && ts.length > 0) {
type = ts[0];
} else {
u2 = u.getWorkingCopy(new NullProgressMonitor());
type = u2.createType("class A {}", null, false, new NullProgressMonitor());
}
}
}
return type != null;
}
public void dispose() {
if (u2 != null) {
try {
u2.discardWorkingCopy();
} catch (JavaModelException e) {
}
u2 = null;
}
u = null;
type = null;
}
/**
* Resolves reference to field constant or class name.
* Returns resolved qualified name. Initial value of the field
* is stored to be retrieved by getConstant().
* @param value
* @return
* @throws CoreException
*/
Object resolveReference(Object value) throws CoreException {
IImportDeclaration[] is = u.getImports();
String stringValue = value.toString();
int lastDot = stringValue.lastIndexOf('.');
String lastToken = stringValue.substring(lastDot + 1);
if(lastDot < 0) {
IField f = (element.getParent() == type) ? type.getField(lastToken) : EclipseJavaUtil.findField(type, lastToken);
if(f != null && f.exists()) {
value = f.getDeclaringType().getFullyQualifiedName() + "." + lastToken;
setFieldInitialValueToConstant(f);
} else {
String v = getFullName(type, is, lastToken);
if(v != null) {
value = v;
String typeName = v.substring(0, v.length() - lastToken.length() - 1);
f = findField(type.getJavaProject(), typeName, lastToken);
if(f != null) {
setFieldInitialValueToConstant(f);
}
}
}
return value;
}
String prefix = stringValue.substring(0, lastDot);
String t = EclipseJavaUtil.resolveType(type, prefix);
if(t != null) {
IType q = EclipseJavaUtil.findType(type.getJavaProject(), t);
if(q != null && q.getField(lastToken).exists()) {
value = t + "." + lastToken;
IField f = q.getField(lastToken);
setFieldInitialValueToConstant(f);
} else {
String v = getFullName(type, is, lastToken);
if(v != null && v.endsWith(stringValue)) {
value = v;
String typeName = v.substring(0, v.length() - lastToken.length() - 1);
IField f = findField(type.getJavaProject(), typeName, lastToken);
if(f != null) {
setFieldInitialValueToConstant(f);
}
}
}
}
return value;
}
}
static Set<String> PRIMITIVE_TYPES = new HashSet<String>();
static {
PRIMITIVE_TYPES.add("int");
PRIMITIVE_TYPES.add("short");
PRIMITIVE_TYPES.add("byte");
PRIMITIVE_TYPES.add("long");
PRIMITIVE_TYPES.add("float");
PRIMITIVE_TYPES.add("double");
PRIMITIVE_TYPES.add("char");
}
private static int findMatchingBrace(String expression, int from, int to) {
int k = 0;
for (int i = from; i < to; i++) {
char c = expression.charAt(i);
if(c == '(') {
k++;
} else if(c == ')') {
k--;
if(k == 0) {
return i + 1;
}
}
}
return -1;
}
private static int findMatchingQuote(String expression, int from, int to) {
char q = expression.charAt(from);
for (int i = from + 1; i < to; i++) {
char c = expression.charAt(i);
if(c == q) return i + 1;
}
return -1;
}
private static Object add(Object left, int operation, Object right) {
if(left == null || right == null) {
return null;
} else if(left instanceof String || right instanceof String) {
if(operation == 0) {
return left.toString() + right;
}
} else if(left instanceof Character) {
return add(new Integer((int)((Character)left).charValue()), operation, right);
} else if(right instanceof Character) {
return add(left, operation, new Integer((int)((Character)right).charValue()));
} else if(left instanceof Number && right instanceof Number) {
if(operation == 0) {
return add((Number)left, (Number)right);
} else if(operation == 1) {
return subtract((Number)left, (Number)right);
}
}
return null;
}
private static Object add(Number left, Number right) {
if(left instanceof Double || right instanceof Double) {
return new Double(left.doubleValue() + right.doubleValue());
} else if(left instanceof Float || right instanceof Float) {
return new Float(left.floatValue() + right.floatValue());
} else if(left instanceof Long || right instanceof Long) {
return new Long(left.longValue() + right.longValue());
} else {
return new Integer(left.intValue() + right.intValue());
}
}
private static Object subtract(Number left, Number right) {
if(left instanceof Double || right instanceof Double) {
return new Double(left.doubleValue() - right.doubleValue());
} else if(left instanceof Float || right instanceof Float) {
return new Float(left.floatValue() - right.floatValue());
} else if(left instanceof Long || right instanceof Long) {
return new Long(left.longValue() - right.longValue());
} else {
return new Integer(left.intValue() - right.intValue());
}
}
private static Object multiply(Object left, Object right) {
if(left == null || right == null) {
return right;
} else if(left instanceof Number && right instanceof Number) {
return multiply((Number)left, (Number)right);
}
return null;
}
private static Object multiply(Number left, Number right) {
if(left instanceof Double || right instanceof Double) {
return new Double(left.doubleValue() * right.doubleValue());
} else if(left instanceof Float || right instanceof Float) {
return new Float(left.floatValue() * right.floatValue());
} else if(left instanceof Long || right instanceof Long) {
return new Long(left.longValue() * right.longValue());
} else {
return new Integer(left.intValue() * right.intValue());
}
}
private static Object divide(Object left, Object right) throws WrongExpressionException {
if(left == null || right == null) {
return right;
} else if(left instanceof Number && right instanceof Number) {
return divide((Number)left, (Number)right);
}
return null;
}
private static Object divide(Number left, Number right) throws WrongExpressionException {
if(isZero((Number)right)) {
throw new WrongExpressionException("Division by zero " + right);
}
if(left instanceof Double || right instanceof Double) {
return new Double( left.doubleValue() / right.doubleValue());
} else if(left instanceof Float || right instanceof Float) {
return new Float(left.floatValue() / right.floatValue());
} else if(left instanceof Long || right instanceof Long) {
return new Long(left.longValue() / right.longValue());
} else {
return new Integer(left.intValue() / right.intValue());
}
}
private static boolean isZero(Number n) {
if(n instanceof Double) {
return Math.abs(n.doubleValue()) < 1E-14;
} else if(n instanceof Float) {
return Math.abs(n.floatValue()) < 1E-7;
} else {
return n.intValue() == 0;
}
}
private static boolean isNameToken(String t) {
if(t.length() == 0) {
return false;
}
if(!Character.isJavaIdentifierStart(t.charAt(0))) {
return false;
}
for (int i = 1; i < t.length(); i++) {
char ch = t.charAt(i);
if(!Character.isJavaIdentifierPart(ch) && ch != '.') {
return false;
}
}
return true;
}
private static String getFullName(IType type, IImportDeclaration[] is, String name) throws CoreException {
for (IImportDeclaration d: is) {
String n = d.getElementName();
if(n.equals(name) || n.endsWith("." + name)) {
return n;
}
if(Flags.isStatic(d.getFlags()) && n.endsWith(".*")) {
String typename = n.substring(0, n.length() - 2);
IType t = EclipseJavaUtil.findType(type.getJavaProject(), typename);
if(t != null && t.exists()) {
IField f = EclipseJavaUtil.findField(t, name);
if(f != null) {
return f.getDeclaringType().getFullyQualifiedName() + "." + name;
}
}
}
}
return null;
}
private static IField findField(IJavaProject jp, String typeName, String fieldName) throws CoreException {
IType t = EclipseJavaUtil.findType(jp, typeName);
if(t == null && typeName.lastIndexOf('.') > 0) {
int i = typeName.lastIndexOf('.');
String innerType = typeName.substring(0, i) + "$" + typeName.substring(i + 1);
t = EclipseJavaUtil.findType(jp, innerType);
}
if(t != null) {
IField f = t.getField(fieldName);
if(f != null && f.exists()) {
return f;
}
}
return null;
}
}