/******************************************************************************* * 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; } }