blob: 83723df6c91996037b324b35e7e2fa89539e3a42 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.codehaus.groovy.runtime;
import groovy.lang.MetaMethod;
import groovy.lang.MetaProperty;
import org.codehaus.groovy.reflection.CachedClass;
import org.codehaus.groovy.reflection.ClassInfo;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* Utility class for MissingMethodException, MissingPropertyException etc.
* This class contains methods assisting in ranking and listing probable intended
* methods/fields when a exception is thrown.
*/
public class MethodRankHelper{
//These are the costs for the various edit operations
//they are used by the two DamerauLevenshtein implementations
public static final int DL_SUBSTITUTION = 10;
public static final int DL_DELETE = 10; //This is also the cost for a insert
public static final int DL_TRANSPOSITION = 5;
public static final int DL_CASE = 5;
public static final int MAX_RECOMENDATIONS = 5;
public static final int MAX_METHOD_SCORE = 50;
public static final int MAX_CONSTRUCTOR_SCORE = 20;
public static final int MAX_FIELD_SCORE = 30;
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
private static final class Pair<U,V> {
private final U u;
private final V v;
public Pair(U u, V v){
this.u = u;
this.v = v;
}
}
/**
* Returns a string detailing possible solutions to a missing method
* if no good solutions can be found a empty string is returned.
*
* @param methodName the name of the method that doesn't exist
* @param type the class on which the method is invoked
* @param arguments the arguments passed to the method
* @return a string with probable solutions to the exception
*/
public static String getMethodSuggestionString(String methodName, Class type, Object[] arguments){
ClassInfo ci = ClassInfo.getClassInfo(type);
List<MetaMethod> methods = new ArrayList<MetaMethod>(ci.getMetaClass().getMethods());
methods.addAll(ci.getMetaClass().getMetaMethods());
List<MetaMethod> sugg = rankMethods(methodName,arguments,methods);
StringBuilder sb = new StringBuilder();
if (!sugg.isEmpty()){
sb.append("\nPossible solutions: ");
for(int i = 0; i < sugg.size(); i++) {
if(i != 0) sb.append(", ");
sb.append(sugg.get(i).getName()).append("(");
sb.append(listParameterNames(sugg.get(i).getParameterTypes()));
sb.append(")");
}
}
Class[] argumentClasses = getArgumentClasses(arguments);
List<Pair<Class,Class>> conflictClasses = getConflictClasses(sugg,argumentClasses);
if (!conflictClasses.isEmpty()){
sb.append("\nThe following classes appear as argument class and as parameter class, ");
sb.append("but are defined by different class loader:\n");
boolean first = true;
for(Pair<Class,Class> pair: conflictClasses) {
if (!first) {
sb.append(", ");
} else {
first = false;
}
sb.append(pair.u.getName()).append(" (defined by '");
sb.append(pair.u.getClassLoader());
sb.append("' and '");
sb.append(pair.v.getClassLoader());
sb.append("')");
}
sb.append("\nIf one of the method suggestions matches the method you wanted to call, ");
sb.append("\nthen check your class loader setup.");
}
return sb.toString();
}
private static List<Pair<Class,Class>> getConflictClasses(List<MetaMethod> sugg, Class[] argumentClasses) {
List<Pair<Class,Class>> ret = new LinkedList<Pair<Class,Class>>();
Set<Class> recordedClasses = new HashSet<Class>();
for (MetaMethod method : sugg) {
Class[] para = method.getNativeParameterTypes();
for (Class aPara : para) {
if (recordedClasses.contains(aPara)) continue;
for (Class argumentClass : argumentClasses) {
if (argumentClass == null) continue;
if (argumentClass == aPara) continue;
if (argumentClass.getName().equals(aPara.getName())) {
ret.add(new Pair<Class, Class>(argumentClass, aPara));
}
}
recordedClasses.add(aPara);
}
}
return ret;
}
private static Class[] getArgumentClasses(Object[] arguments) {
Class[] argumentClasses = new Class[arguments.length];
for (int i=0; i<argumentClasses.length; i++) {
Object arg = arguments[i];
if (arg==null) continue;
argumentClasses[i] = arg.getClass();
}
return argumentClasses;
}
/**
* Returns a string detailing possible solutions to a missing constructor
* if no good solutions can be found a empty string is returned.
*
* @param arguments the arguments passed to the constructor
* @param type the class on which the constructor is invoked
* @return a string with probable solutions to the exception
*/
public static String getConstructorSuggestionString(Class type, Object[] arguments){
Constructor[] sugg = rankConstructors(arguments, type.getConstructors());
if(sugg.length >0){
StringBuilder sb = new StringBuilder();
sb.append("\nPossible solutions: ");
for(int i = 0; i < sugg.length; i++){
if(i != 0) sb.append(", ");
sb.append(type.getName()).append("(");
sb.append(listParameterNames(sugg[i].getParameterTypes()));
sb.append(")");
}
return sb.toString();
} else{
return "";
}
}
/**
* Returns a string detailing possible solutions to a missing field or property
* if no good solutions can be found a empty string is returned.
*
* @param fieldName the missing field
* @param type the class on which the field is sought
* @return a string with probable solutions to the exception
*/
public static String getPropertySuggestionString(String fieldName, Class type){
ClassInfo ci = ClassInfo.getClassInfo(type);
List<MetaProperty> fi = ci.getMetaClass().getProperties();
List<RankableField> rf = new ArrayList<RankableField>(fi.size());
StringBuilder sb = new StringBuilder();
sb.append("\nPossible solutions: ");
for(MetaProperty mp : fi) rf.add(new RankableField(fieldName, mp));
Collections.sort(rf);
int i = 0;
for (RankableField f : rf) {
if (i > MAX_RECOMENDATIONS) break;
if (f.score > MAX_FIELD_SCORE) break;
if(i > 0) sb.append(", ");
sb.append(f.f.getName());
i++;
}
return i > 0? sb.toString(): "";
}
/**
* creates a comma separated list of each of the class names.
*
* @param cachedClasses the array of Classes
* @return the Class names
*/
private static String listParameterNames(Class[] cachedClasses){
StringBuilder sb = new StringBuilder();
for(int i =0; i < cachedClasses.length;i++){
if(i != 0) sb.append(", ");
sb.append(cachedClasses[i].getName());
}
return sb.toString();
}
private static String listParameterNames(CachedClass[] cachedClasses){
StringBuilder sb = new StringBuilder();
for(int i =0; i < cachedClasses.length;i++){
if(i != 0) sb.append(", ");
sb.append(cachedClasses[i].getName());
}
return sb.toString();
}
/**
* Returns a sorted(ranked) list of a selection of the methods among candidates which
* most closely resembles original.
*
* @param name
* @param original
* @param methods
* @return a sorted lists of Methods
*/
private static List<MetaMethod> rankMethods(String name, Object[] original, List<MetaMethod> methods) {
List<RankableMethod> rm = new ArrayList<RankableMethod>(methods.size());
if (original==null) original = EMPTY_OBJECT_ARRAY;
Class[] ta = new Class[original.length];
Class nullC = NullObject.class;
for(int i = 0; i < original.length; i++){
//All nulls have to be wrapped so that they can be compared
ta[i] = original[i] == null?nullC: original[i].getClass();
}
for (MetaMethod m:methods) {
rm.add(new RankableMethod(name, ta, m));
}
Collections.sort(rm);
List<MetaMethod> l = new ArrayList<MetaMethod>(rm.size());
for (RankableMethod m : rm) {
if (l.size() > MAX_RECOMENDATIONS) break;
if (m.score > MAX_METHOD_SCORE) break;
l.add(m.m);
}
return l;
}
/**
* This class wraps a method object and a score variable so methods
* Can easily be ranked by their likeness to a another method
*
*/
private static final class RankableMethod implements Comparable {
final MetaMethod m;
final Integer score;
public RankableMethod(String name, Class[] argumentTypes, MetaMethod m2) {
this.m = m2;
int nameDist = delDistance(name, m2.getName());
//unbox primitives
Class[] mArgs = new Class[m2.getParameterTypes().length];
for(int i =0; i < mArgs.length; i++){
//All args have to be boxed since argumentTypes is always boxed
mArgs[i] = boxVar(m2.getParameterTypes()[i].getTheClass());
}
int argDist = damerauLevenshteinDistance(argumentTypes,mArgs);
this.score = nameDist + argDist;
}
public int compareTo(Object o) {
RankableMethod mo = (RankableMethod) o;
return score.compareTo(mo.score);
}
}
/**
* Returns a sorted(ranked) list of a selection of the constructors among candidates which
* most closely resembles original.
*
* @param original
* @param candidates
* @return a sorted lists of Methods
*/
private static Constructor[] rankConstructors(Object[] original, Constructor[] candidates) {
RankableConstructor[] rc = new RankableConstructor[candidates.length];
Class[] ta = new Class[original.length];
Class nullC = NullObject.class;
for (int i = 0; i < original.length; i++) {
//All nulls have to be wrapped so that they can be compared
ta[i] = original[i] == null ? nullC : original[i].getClass();
}
for (int i = 0; i < candidates.length; i++) {
rc[i] = new RankableConstructor(ta, candidates[i]);
}
Arrays.sort(rc);
List<Constructor> l = new ArrayList<Constructor>();
int index = 0;
while (l.size() < MAX_RECOMENDATIONS && index < rc.length && rc[index].score < MAX_CONSTRUCTOR_SCORE) {
l.add(rc[index].c);
index++;
}
return l.toArray(new Constructor[0]);
}
/**
* This class wraps a method object and a score variable so methods
* Can easily be ranked by their likeness to a another method
*
*/
private static final class RankableConstructor implements Comparable {
final Constructor c;
final Integer score;
public RankableConstructor(Class[] argumentTypes, Constructor c) {
this.c = c;
//unbox primitives
Class[] cArgs = new Class[c.getParameterTypes().length];
for(int i =0; i < cArgs.length; i++){
//All args have to be boxed since argumentTypes is always boxed
cArgs[i] = boxVar(c.getParameterTypes()[i]);
}
this.score = damerauLevenshteinDistance(argumentTypes,cArgs);
}
public int compareTo(Object o) {
RankableConstructor co = (RankableConstructor) o;
return score.compareTo(co.score);
}
}
/**
* This class wraps a method object and a score variable so methods
* Can easily be ranked by their likeness to a another method
*
*/
private static final class RankableField implements Comparable {
final MetaProperty f;
final Integer score;
public RankableField(String name, MetaProperty mp) {
this.f = mp;
this.score = delDistance(name,mp.getName());
}
public int compareTo(Object o) {
RankableField co = (RankableField) o;
return score.compareTo(co.score);
}
}
/**
* If c is a primitive class this method returns a boxed version
* otherwise c is returned.
* In java 1.5 this can be simplified thanks to the Type class.
* @param c
* @return a boxed version of c if c can be boxed, else c
*/
protected static Class boxVar(Class c){
if(Boolean.TYPE.equals(c)){
return Boolean.class;
}else if(Character.TYPE.equals(c)){
return Character.class;
}else if(Byte.TYPE.equals(c)){
return Byte.class;
}else if(Double.TYPE.equals(c)){
return Double.class;
}else if(Float.TYPE.equals(c)){
return Float.class;
}else if(Integer.TYPE.equals(c)){
return Integer.class;
}else if(Long.TYPE.equals(c)){
return Long.class;
}else if(Short.TYPE.equals(c)){
return Short.class;
}else{
return c;
}
}
/**
* This is a small wrapper for nulls
*/
private static class NullObject{
}
/**
* This is a slightly modified version of the Damerau Levenshtein distance
* algorithm. It has a additional test to see if a character has switched case,
* in the original algorithm this counts as a substitution.
* The "cost" for a substitution is given as 10 instead of 1 in this version,
* this enables transpositions and case modifications to have a lower cost than
* substitutions.
*
* Currently the lowercase versions of t_j and s_i isn't cached, its probable
* that some speed could be gained from this.
*
* This version is based on Chas Emerick's implementation of Levenshtein Distance
* for jakarta commons.
* @param s a CharSequence
* @param t the CharSequence to be compared to s
* @return a value representing the edit distance between s and t
*/
public static int delDistance(CharSequence s, CharSequence t) {
if (s == null || t == null) {
throw new IllegalArgumentException("Strings must not be null");
}
int n = s.length(); // length of s
int m = t.length(); // length of t
if (n == 0) {
return m;
} else if (m == 0) {
return n;
}
//we have to keep 3 rows instead of the 2 used in Levenshtein
int[][] vals = new int[3][n + 1];
int _d[]; //placeholder to assist in rotating vals
// indexes into strings s and t
int i; // iterates through s
int j; // iterates through t
char t_j; // jth character of t
char s_i; // ith character of s
int cost; // cost
for (i = 0; i <= n; i++) {
vals[1][i] = i * DL_DELETE;
}
for (j = 1; j <= m; j++) {
t_j = t.charAt(j - 1);
vals[0][0] = j * DL_DELETE;
for (i = 1; i <= n; i++) {
s_i = s.charAt(i - 1);
if (Character.isLowerCase(s_i) ^ Character.isLowerCase(t_j)) {
//if s_i and t_i don't have have the same case
cost = caselessCompare(s_i, t_j) ? DL_CASE : DL_SUBSTITUTION;
} else {
//if they share case check for substitution
cost = s_i == t_j ? 0 : DL_SUBSTITUTION;
}
// minimum of cell to the left+1, to the top+1, diagonally left and up +cost
vals[0][i] = Math.min(Math.min(vals[0][i - 1] + DL_DELETE, vals[1][i] + DL_DELETE), vals[1][i - 1] + cost);
//Check for transposition, somewhat more complex now since we have to check for case
if (i > 1 && j > 1) {
cost = Character.isLowerCase(s_i) ^ Character.isLowerCase(t.charAt(j - 2)) ? DL_CASE : 0;
cost = Character.isLowerCase(s.charAt(i - 2)) ^ Character.isLowerCase(t_j) ? cost + DL_CASE : cost;
if (caselessCompare(s_i, t.charAt(j - 2)) && caselessCompare(s.charAt(i - 2), t_j)) {
vals[0][i] = Math.min(vals[0][i], vals[2][i - 2] + DL_TRANSPOSITION + cost);
}
}
}
// rotate all value arrays upwards(older rows get a higher index)
_d = vals[2];
vals[2] = vals[1];
vals[1] = vals[0];
vals[0] = _d;
}
// our last action in the above loop was to rotate vals, so vals[1] now
// actually has the most recent cost counts
return vals[1][n];
}
/**
* Compares two characters whilst ignoring case.
* @param a the first character
* @param b the second character
* @return true if the characters are equal
*/
private static boolean caselessCompare(char a, char b){
return Character.toLowerCase(a) == Character.toLowerCase(b);
}
/**
* This is a implementation of DL distance between two Object arrays instead
* of character streams. The objects are compared using their equals method.
* No objects may be null.
* This implementation is based on Chas Emerick's implementation of Levenshtein Distance
* for jakarta commons.
* @param s an Object array
* @param t this array is compared to s
* @return the edit distance between the two arrays
*/
public static int damerauLevenshteinDistance(Object[] s, Object[] t) {
if (s == null || t == null) {
throw new IllegalArgumentException("Arrays must not be null");
}
int n = s.length; // length of s
int m = t.length; // length of t
if (n == 0) {
return m;
} else if (m == 0) {
return n;
}
int[][] vals = new int[3][n + 1];
int _d[]; //placeholder to assist in rotating vals
// indexes into arrays s and t
int i; // iterates through s
int j; // iterates through t
Object t_j; // jth object of t
int cost; // cost
for (i = 0; i <= n; i++) {
vals[1][i] = i * DL_DELETE ;
}
for (j = 1; j <= m; j++) {
t_j = t[j - 1];
vals[0][0] = j * DL_DELETE ;
for (i = 1; i <= n; i++) {
cost = s[i - 1].equals(t_j)? 0 : DL_SUBSTITUTION;
// minimum of cell to the left+1, to the top+1, diagonally left and up +cost
vals[0][i] = Math.min(Math.min(vals[0][i - 1] + DL_DELETE, vals[1][i] + DL_DELETE), vals[1][i - 1] + cost);
//Check for transposition
if(i > 1 && j > 1 && s[i -1].equals(t[j -2]) && s[i- 2].equals(t_j)){
vals[0][i] = Math.min(vals[0][i], vals[2][i-2] + DL_TRANSPOSITION);
}
}
// rotate all value arrays upwards(older rows get a higher index)
_d = vals[2];
vals[2] = vals[1];
vals[1] = vals[0];
vals[0] = _d;
}
return vals[1][n];
}
}