http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/runtime/MetaClassHelper.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/runtime/MetaClassHelper.java b/src/main/java/org/codehaus/groovy/runtime/MetaClassHelper.java new file mode 100644 index 0000000..7839a59 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/runtime/MetaClassHelper.java @@ -0,0 +1,1034 @@ +/* + * 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.Closure; +import groovy.lang.GString; +import groovy.lang.GroovyObject; +import groovy.lang.GroovyRuntimeException; +import groovy.lang.MetaClass; +import groovy.lang.MetaMethod; +import org.codehaus.groovy.reflection.CachedClass; +import org.codehaus.groovy.reflection.ParameterTypes; +import org.codehaus.groovy.reflection.ReflectionCache; +import org.codehaus.groovy.runtime.wrappers.Wrapper; +import org.codehaus.groovy.util.FastArray; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author John Wilson + * @author Jochen Theodorou + */ +public class MetaClassHelper { + + public static final Object[] EMPTY_ARRAY = {}; + public static final Class[] EMPTY_TYPE_ARRAY = {}; + public static final Object[] ARRAY_WITH_NULL = {null}; + protected static final Logger LOG = Logger.getLogger(MetaClassHelper.class.getName()); + private static final int MAX_ARG_LEN = 12; + private static final int + OBJECT_SHIFT = 23, INTERFACE_SHIFT = 0, + PRIMITIVE_SHIFT = 21, VARGS_SHIFT = 44; + /* dist binary layout: + * 0-20: interface + * 21-22: primitive dist + * 23-43: object dist + * 44-48: vargs penalty + */ + + public static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + + public static boolean accessibleToConstructor(final Class at, final Constructor constructor) { + boolean accessible = false; + final int modifiers = constructor.getModifiers(); + if (Modifier.isPublic(modifiers)) { + accessible = true; + } else if (Modifier.isPrivate(modifiers)) { + accessible = at.getName().equals(constructor.getName()); + } else if (Modifier.isProtected(modifiers)) { + Boolean isAccessible = checkCompatiblePackages(at, constructor); + if (isAccessible != null) { + accessible = isAccessible; + } else { + boolean flag = false; + Class clazz = at; + while (!flag && clazz != null) { + if (clazz.equals(constructor.getDeclaringClass())) { + flag = true; + break; + } + if (clazz.equals(Object.class)) { + break; + } + clazz = clazz.getSuperclass(); + } + accessible = flag; + } + } else { + Boolean isAccessible = checkCompatiblePackages(at, constructor); + if (isAccessible != null) { + accessible = isAccessible; + } + } + return accessible; + } + + private static Boolean checkCompatiblePackages(Class at, Constructor constructor) { + if (at.getPackage() == null && constructor.getDeclaringClass().getPackage() == null) { + return Boolean.TRUE; + } + if (at.getPackage() == null && constructor.getDeclaringClass().getPackage() != null) { + return Boolean.FALSE; + } + if (at.getPackage() != null && constructor.getDeclaringClass().getPackage() == null) { + return Boolean.FALSE; + } + if (at.getPackage().equals(constructor.getDeclaringClass().getPackage())) { + return Boolean.TRUE; + } + return null; + } + + public static Object[] asWrapperArray(Object parameters, Class componentType) { + Object[] ret = null; + if (componentType == boolean.class) { + boolean[] array = (boolean[]) parameters; + ret = new Object[array.length]; + for (int i = 0; i < array.length; i++) { + ret[i] = array[i]; + } + } else if (componentType == char.class) { + char[] array = (char[]) parameters; + ret = new Object[array.length]; + for (int i = 0; i < array.length; i++) { + ret[i] = array[i]; + } + } else if (componentType == byte.class) { + byte[] array = (byte[]) parameters; + ret = new Object[array.length]; + for (int i = 0; i < array.length; i++) { + ret[i] = array[i]; + } + } else if (componentType == int.class) { + int[] array = (int[]) parameters; + ret = new Object[array.length]; + for (int i = 0; i < array.length; i++) { + ret[i] = array[i]; + } + } else if (componentType == short.class) { + short[] array = (short[]) parameters; + ret = new Object[array.length]; + for (int i = 0; i < array.length; i++) { + ret[i] = array[i]; + } + } else if (componentType == long.class) { + long[] array = (long[]) parameters; + ret = new Object[array.length]; + for (int i = 0; i < array.length; i++) { + ret[i] = array[i]; + } + } else if (componentType == double.class) { + double[] array = (double[]) parameters; + ret = new Object[array.length]; + for (int i = 0; i < array.length; i++) { + ret[i] = array[i]; + } + } else if (componentType == float.class) { + float[] array = (float[]) parameters; + ret = new Object[array.length]; + for (int i = 0; i < array.length; i++) { + ret[i] = array[i]; + } + } + + return ret; + } + + + /** + * @param list the original list + * @param parameterType the resulting array type + * @return the constructed array + */ + public static Object asPrimitiveArray(List list, Class parameterType) { + Class arrayType = parameterType.getComponentType(); + Object objArray = Array.newInstance(arrayType, list.size()); + for (int i = 0; i < list.size(); i++) { + Object obj = list.get(i); + if (arrayType.isPrimitive()) { + if (obj instanceof Integer) { + Array.setInt(objArray, i, (Integer) obj); + } else if (obj instanceof Double) { + Array.setDouble(objArray, i, (Double) obj); + } else if (obj instanceof Boolean) { + Array.setBoolean(objArray, i, (Boolean) obj); + } else if (obj instanceof Long) { + Array.setLong(objArray, i, (Long) obj); + } else if (obj instanceof Float) { + Array.setFloat(objArray, i, (Float) obj); + } else if (obj instanceof Character) { + Array.setChar(objArray, i, (Character) obj); + } else if (obj instanceof Byte) { + Array.setByte(objArray, i, (Byte) obj); + } else if (obj instanceof Short) { + Array.setShort(objArray, i, (Short) obj); + } + } else { + Array.set(objArray, i, obj); + } + } + return objArray; + } + + private static final Class[] PRIMITIVES = { + boolean.class, + Boolean.class, + byte.class, + Byte.class, + short.class, + Short.class, + char.class, + Character.class, + int.class, + Integer.class, + long.class, + Long.class, + BigInteger.class, + float.class, + Float.class, + double.class, + Double.class, + BigDecimal.class, + Number.class, + Object.class + }; + + private static final int[][] PRIMITIVE_DISTANCE_TABLE = { + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + /*boolean[0]*/ { 0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2,}, + /*Boolean[1]*/ { 1, 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2,}, + /*byte[2]*/ {18, 19, 0, 1, 2, 3, 16, 17, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,}, + /*Byte[3]*/ {18, 19, 1, 0, 2, 3, 16, 17, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,}, + /*short[4]*/ {18, 19, 14, 15, 0, 1, 16, 17, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,}, + /*Short[5]*/ {18, 19, 14, 15, 1, 0, 16, 17, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,}, + /*char[6]*/ {18, 19, 16, 17, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,}, + /*Character[7]*/ {18, 19, 16, 17, 14, 15, 1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,}, + /*int[8]*/ {18, 19, 14, 15, 12, 13, 16, 17, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,}, + /*Integer[9]*/ {18, 19, 14, 15, 12, 13, 16, 17, 1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,}, + /*long[10]*/ {18, 19, 14, 15, 12, 13, 16, 17, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,}, + /*Long[11]*/ {18, 19, 14, 15, 12, 13, 16, 17, 10, 11, 1, 0, 2, 3, 4, 5, 6, 7, 8, 9,}, + /*BigInteger[12]*/ {18, 19, 9, 10, 7, 8, 16, 17, 5, 6, 3, 4, 0, 14, 15, 12, 13, 11, 1, 2,}, + /*float[13]*/ {18, 19, 14, 15, 12, 13, 16, 17, 10, 11, 8, 9, 7, 0, 1, 2, 3, 4, 5, 6,}, + /*Float[14]*/ {18, 19, 14, 15, 12, 13, 16, 17, 10, 11, 8, 9, 7, 1, 0, 2, 3, 4, 5, 6,}, + /*double[15]*/ {18, 19, 14, 15, 12, 13, 16, 17, 10, 11, 8, 9, 7, 5, 6, 0, 1, 2, 3, 4,}, + /*Double[16]*/ {18, 19, 14, 15, 12, 13, 16, 17, 10, 11, 8, 9, 7, 5, 6, 1, 0, 2, 3, 4,}, + /*BigDecimal[17]*/ {18, 19, 14, 15, 12, 13, 16, 17, 10, 11, 8, 9, 7, 5, 6, 3, 4, 0, 1, 2,}, + /*Number[18]*/ {18, 19, 14, 15, 12, 13, 16, 17, 10, 11, 8, 9, 7, 5, 6, 3, 4, 2, 0, 1,}, + /*Object[19]*/ {18, 19, 14, 15, 12, 13, 16, 17, 10, 11, 8, 9, 7, 5, 6, 3, 4, 2, 1, 0,}, + }; + + private static int getPrimitiveIndex(Class c) { + for (byte i = 0; i < PRIMITIVES.length; i++) { + if (PRIMITIVES[i] == c) return i; + } + return -1; + } + + private static int getPrimitiveDistance(Class from, Class to) { + // we know here that from!=to, so a distance of 0 is never valid + // get primitive type indexes + int fromIndex = getPrimitiveIndex(from); + int toIndex = getPrimitiveIndex(to); + if (fromIndex == -1 || toIndex == -1) return -1; + return PRIMITIVE_DISTANCE_TABLE[toIndex][fromIndex]; + } + + private static int getMaximumInterfaceDistance(Class c, Class interfaceClass) { + // -1 means a mismatch + if (c == null) return -1; + // 0 means a direct match + if (c == interfaceClass) return 0; + Class[] interfaces = c.getInterfaces(); + int max = -1; + for (Class anInterface : interfaces) { + int sub = getMaximumInterfaceDistance(anInterface, interfaceClass); + // we need to keep the -1 to track the mismatch, a +1 + // by any means could let it look like a direct match + // we want to add one, because there is an interface between + // the interface we search for and the interface we are in. + if (sub != -1) sub++; + // we are interested in the longest path only + max = Math.max(max, sub); + } + // we do not add one for super classes, only for interfaces + int superClassMax = getMaximumInterfaceDistance(c.getSuperclass(), interfaceClass); + if (superClassMax != -1) superClassMax++; + return Math.max(max, superClassMax); + } + + private static long calculateParameterDistance(Class argument, CachedClass parameter) { + /** + * note: when shifting with 32 bit, you should only shift on a long. If you do + * that with an int, then i==(i<<32), which means you loose the shift + * information + */ + + if (parameter.getTheClass() == argument) return 0; + + if (parameter.isInterface()) { + int dist = getMaximumInterfaceDistance(argument, parameter.getTheClass()) << INTERFACE_SHIFT; + if (dist>-1 || !(argument!=null && Closure.class.isAssignableFrom(argument))) { + return dist; + } // else go to object case + } + + long objectDistance = 0; + if (argument != null) { + long pd = getPrimitiveDistance(parameter.getTheClass(), argument); + if (pd != -1) return pd << PRIMITIVE_SHIFT; + + // add one to dist to be sure interfaces are preferred + objectDistance += PRIMITIVES.length + 1; + + // GROOVY-5114 : if we have to choose between two methods + // foo(Object[]) and foo(Object) and that the argument is an array type + // then the array version should be preferred + if (argument.isArray() && !parameter.isArray) { + objectDistance+=4; + } + Class clazz = ReflectionCache.autoboxType(argument); + while (clazz != null) { + if (clazz == parameter.getTheClass()) break; + if (clazz == GString.class && parameter.getTheClass() == String.class) { + objectDistance += 2; + break; + } + clazz = clazz.getSuperclass(); + objectDistance += 3; + } + } else { + // choose the distance to Object if a parameter is null + // this will mean that Object is preferred over a more + // specific type + Class clazz = parameter.getTheClass(); + if (clazz.isPrimitive()) { + objectDistance += 2; + } else { + while (clazz != Object.class && clazz != null) { + clazz = clazz.getSuperclass(); + objectDistance += 2; + } + } + } + return objectDistance << OBJECT_SHIFT; + } + + public static long calculateParameterDistance(Class[] arguments, ParameterTypes pt) { + CachedClass[] parameters = pt.getParameterTypes(); + if (parameters.length == 0) return 0; + + long ret = 0; + int noVargsLength = parameters.length - 1; + + // if the number of parameters does not match we have + // a vargs usage + // + // case A: arguments.length<parameters.length + // + // In this case arguments.length is always equal to + // noVargsLength because only the last parameter + // might be a optional vargs parameter + // + // VArgs penalty: 1l + // + // case B: arguments.length>parameters.length + // + // In this case all arguments with a index bigger than + // paramMinus1 are part of the vargs, so a + // distance calculation needs to be done against + // parameters[noVargsLength].getComponentType() + // + // VArgs penalty: 2l+arguments.length-parameters.length + // + // case C: arguments.length==parameters.length && + // isAssignableFrom( parameters[noVargsLength], + // arguments[noVargsLength] ) + // + // In this case we have no vargs, so calculate directly + // + // VArgs penalty: 0l + // + // case D: arguments.length==parameters.length && + // !isAssignableFrom( parameters[noVargsLength], + // arguments[noVargsLength] ) + // + // In this case we have a vargs case again, we need + // to calculate arguments[noVargsLength] against + // parameters[noVargsLength].getComponentType + // + // VArgs penalty: 2l + // + // This gives: VArgs_penalty(C)<VArgs_penalty(A) + // VArgs_penalty(A)<VArgs_penalty(D) + // VArgs_penalty(D)<VArgs_penalty(B) + + /** + * In general we want to match the signature that allows us to use + * as less arguments for the vargs part as possible. That means the + * longer signature usually wins if both signatures are vargs, while + * vargs looses always against a signature without vargs. + * + * A vs B : + * def foo(Object[] a) {1} -> case B + * def foo(a,b,Object[] c) {2} -> case A + * assert foo(new Object(),new Object()) == 2 + * --> A preferred over B + * + * A vs C : + * def foo(Object[] a) {1} -> case B + * def foo(a,b) {2} -> case C + * assert foo(new Object(),new Object()) == 2 + * --> C preferred over A + * + * A vs D : + * def foo(Object[] a) {1} -> case D + * def foo(a,Object[] b) {2} -> case A + * assert foo(new Object()) == 2 + * --> A preferred over D + * + * This gives C<A<B,D + * + * B vs C : + * def foo(Object[] a) {1} -> case B + * def foo(a,b) {2} -> case C + * assert foo(new Object(),new Object()) == 2 + * --> C preferred over B, matches C<A<B,D + * + * B vs D : + * def foo(Object[] a) {1} -> case B + * def foo(a,Object[] b) {2} -> case D + * assert foo(new Object(),new Object()) == 2 + * --> D preferred over B + * + * This gives C<A<D<B + */ + + // first we calculate all arguments, that are for sure not part + // of vargs. Since the minimum for arguments is noVargsLength + // we can safely iterate to this point + for (int i = 0; i < noVargsLength; i++) { + ret += calculateParameterDistance(arguments[i], parameters[i]); + } + + if (arguments.length == parameters.length) { + // case C&D, we use baseType to calculate and set it + // to the value we need according to case C and D + CachedClass baseType = parameters[noVargsLength]; // case C + if (!parameters[noVargsLength].isAssignableFrom(arguments[noVargsLength])) { + baseType = ReflectionCache.getCachedClass(baseType.getTheClass().getComponentType()); // case D + ret += 2L << VARGS_SHIFT; // penalty for vargs + } + ret += calculateParameterDistance(arguments[noVargsLength], baseType); + } else if (arguments.length > parameters.length) { + // case B + // we give our a vargs penalty for each exceeding argument and iterate + // by using parameters[noVargsLength].getComponentType() + ret += (2L + arguments.length - parameters.length) << VARGS_SHIFT; // penalty for vargs + CachedClass vargsType = ReflectionCache.getCachedClass(parameters[noVargsLength].getTheClass().getComponentType()); + for (int i = noVargsLength; i < arguments.length; i++) { + ret += calculateParameterDistance(arguments[i], vargsType); + } + } else { + // case A + // we give a penalty for vargs, since we have no direct + // match for the last argument + ret += 1L << VARGS_SHIFT; + } + + return ret; + } + + /** + * This is the complement to the java.beans.Introspector.decapitalize(String) method. + * We handle names that begin with an initial lowerCase followed by upperCase specially + * (which is to make no change). + * See GROOVY-3211. + * + * @param property the property name to capitalize + * @return the name capitalized, except when we don't + */ + public static String capitalize(final String property) { + final String rest = property.substring(1); + + // Funky rule so that names like 'pNAME' will still work. + if (Character.isLowerCase(property.charAt(0)) && (rest.length() > 0) && Character.isUpperCase(rest.charAt(0))) { + return property; + } + + return property.substring(0, 1).toUpperCase() + rest; + } + + /** + * @param methods the methods to choose from + * @return the method with 1 parameter which takes the most general type of + * object (e.g. Object) + */ + public static Object chooseEmptyMethodParams(FastArray methods) { + Object vargsMethod = null; + final int len = methods.size(); + final Object[] data = methods.getArray(); + for (int i = 0; i != len; ++i) { + Object method = data[i]; + final ParameterTypes pt = (ParameterTypes) method; + CachedClass[] paramTypes = pt.getParameterTypes(); + int paramLength = paramTypes.length; + if (paramLength == 0) { + return method; + } else if (paramLength == 1 && pt.isVargsMethod(EMPTY_ARRAY)) { + vargsMethod = method; + } + } + return vargsMethod; + } + + /** + * Warning: this method does not choose properly if multiple methods with + * the same distance are encountered + * @param methods the methods to choose from + * @return the method with 1 parameter which takes the most general type of + * object (e.g. Object) ignoring primitive types + * @deprecated + */ + @Deprecated + public static Object chooseMostGeneralMethodWith1NullParam(FastArray methods) { + // let's look for methods with 1 argument which matches the type of the + // arguments + CachedClass closestClass = null; + CachedClass closestVargsClass = null; + Object answer = null; + int closestDist = -1; + final int len = methods.size(); + for (int i = 0; i != len; ++i) { + final Object[] data = methods.getArray(); + Object method = data[i]; + final ParameterTypes pt = (ParameterTypes) method; + CachedClass[] paramTypes = pt.getParameterTypes(); + int paramLength = paramTypes.length; + if (paramLength == 0 || paramLength > 2) continue; + + CachedClass theType = paramTypes[0]; + if (theType.isPrimitive) continue; + + if (paramLength == 2) { + if (!pt.isVargsMethod(ARRAY_WITH_NULL)) continue; + if (closestClass == null) { + closestVargsClass = paramTypes[1]; + closestClass = theType; + answer = method; + } else if (closestClass.getTheClass() == theType.getTheClass()) { + if (closestVargsClass == null) continue; + CachedClass newVargsClass = paramTypes[1]; + if (isAssignableFrom(newVargsClass.getTheClass(), closestVargsClass.getTheClass())) { + closestVargsClass = newVargsClass; + answer = method; + } + } else if (isAssignableFrom(theType.getTheClass(), closestClass.getTheClass())) { + closestVargsClass = paramTypes[1]; + closestClass = theType; + answer = method; + } + } else { + if (closestClass == null || isAssignableFrom(theType.getTheClass(), closestClass.getTheClass())) { + closestVargsClass = null; + closestClass = theType; + answer = method; + closestDist = -1; + } else { + // closestClass and theType are not in a subtype relation, we need + // to check the distance to Object + if (closestDist == -1) closestDist = closestClass.getSuperClassDistance(); + int newDist = theType.getSuperClassDistance(); + if (newDist < closestDist) { + closestDist = newDist; + closestVargsClass = null; + closestClass = theType; + answer = method; + } + } + } + } + return answer; + } + + // + + /** + * @param list a list of MetaMethods + * @param method the MetaMethod of interest + * @return true if a method of the same matching prototype was found in the + * list + */ + public static boolean containsMatchingMethod(List list, MetaMethod method) { + for (Object aList : list) { + MetaMethod aMethod = (MetaMethod) aList; + CachedClass[] params1 = aMethod.getParameterTypes(); + CachedClass[] params2 = method.getParameterTypes(); + if (params1.length == params2.length) { + boolean matches = true; + for (int i = 0; i < params1.length; i++) { + if (params1[i] != params2[i]) { + matches = false; + break; + } + } + if (matches) { + return true; + } + } + } + return false; + } + + /** + * param instance array to the type array + * + * @param args the arguments + * @return the types of the arguments + */ + public static Class[] convertToTypeArray(Object[] args) { + if (args == null) + return null; + int s = args.length; + Class[] ans = new Class[s]; + for (int i = 0; i < s; i++) { + Object o = args[i]; + ans[i] = getClassWithNullAndWrapper(o); + } + return ans; + } + + public static Object makeCommonArray(Object[] arguments, int offset, Class fallback) { + // arguments.length>0 && !=null + Class baseClass = null; + for (int i = offset; i < arguments.length; i++) { + if (arguments[i] == null) continue; + Class argClass = arguments[i].getClass(); + if (baseClass == null) { + baseClass = argClass; + } else { + for (; baseClass != Object.class; baseClass = baseClass.getSuperclass()) { + if (baseClass.isAssignableFrom(argClass)) break; + } + } + } + if (baseClass == null) { + // all arguments were null + baseClass = fallback; + } + /* + * If no specific super class has been found and type fallback is an interface, check if all arg classes + * implement it. If yes, then that interface is the common type across arguments. + */ + if (baseClass == Object.class && fallback.isInterface()) { + int tmpCount = 0; + for (int i = offset; i < arguments.length; i++) { + if (arguments[i] != null) { + Class tmpClass; + Set<Class> intfs = new HashSet<Class>(); + tmpClass = arguments[i].getClass(); + for (; tmpClass != Object.class; tmpClass = tmpClass.getSuperclass()) { + intfs.addAll(Arrays.asList(tmpClass.getInterfaces())); + } + if (intfs.contains(fallback)) { + tmpCount++; + } + } + } + // all arg classes implement interface fallback, so use that as the array component type + if (tmpCount == arguments.length - offset) { + baseClass = fallback; + } + } + Object result = makeArray(null, baseClass, arguments.length - offset); + System.arraycopy(arguments, offset, result, 0, arguments.length - offset); + return result; + } + + public static Object makeArray(Object obj, Class secondary, int length) { + Class baseClass = secondary; + if (obj != null) { + baseClass = obj.getClass(); + } + /*if (GString.class.isAssignableFrom(baseClass)) { + baseClass = GString.class; + }*/ + return Array.newInstance(baseClass, length); + } + + public static GroovyRuntimeException createExceptionText(String init, MetaMethod method, Object object, Object[] args, Throwable reason, boolean setReason) { + return new GroovyRuntimeException( + init + + method + + " on: " + + object + + " with arguments: " + + InvokerHelper.toString(args) + + " reason: " + + reason, + setReason ? reason : null); + } + + protected static String getClassName(Object object) { + if (object == null) return null; + return (object instanceof Class) ? ((Class) object).getName() : object.getClass().getName(); + } + + /** + * Returns a callable object for the given method name on the object. + * The object acts like a Closure in that it can be called, like a closure + * and passed around - though really its a method pointer, not a closure per se. + * + * @param object the object containing the method + * @param methodName the method of interest + * @return the resulting closure-like method pointer + */ + public static Closure getMethodPointer(Object object, String methodName) { + return new MethodClosure(object, methodName); + } + + public static boolean isAssignableFrom(Class classToTransformTo, Class classToTransformFrom) { + if (classToTransformTo == classToTransformFrom + || classToTransformFrom == null + || classToTransformTo == Object.class) { + return true; + } + + classToTransformTo = ReflectionCache.autoboxType(classToTransformTo); + classToTransformFrom = ReflectionCache.autoboxType(classToTransformFrom); + if (classToTransformTo == classToTransformFrom) return true; + + // note: there is no coercion for boolean and char. Range matters, precision doesn't + if (classToTransformTo == Integer.class) { + if (classToTransformFrom == Short.class + || classToTransformFrom == Byte.class + || classToTransformFrom == BigInteger.class) + return true; + } else if (classToTransformTo == Double.class) { + if (classToTransformFrom == Integer.class + || classToTransformFrom == Long.class + || classToTransformFrom == Short.class + || classToTransformFrom == Byte.class + || classToTransformFrom == Float.class + || classToTransformFrom == BigDecimal.class + || classToTransformFrom == BigInteger.class) + return true; + } else if (classToTransformTo == BigDecimal.class) { + if (classToTransformFrom == Double.class + || classToTransformFrom == Integer.class + || classToTransformFrom == Long.class + || classToTransformFrom == Short.class + || classToTransformFrom == Byte.class + || classToTransformFrom == Float.class + || classToTransformFrom == BigInteger.class) + return true; + } else if (classToTransformTo == BigInteger.class) { + if (classToTransformFrom == Integer.class + || classToTransformFrom == Long.class + || classToTransformFrom == Short.class + || classToTransformFrom == Byte.class) + return true; + } else if (classToTransformTo == Long.class) { + if (classToTransformFrom == Integer.class + || classToTransformFrom == Short.class + || classToTransformFrom == Byte.class) + return true; + } else if (classToTransformTo == Float.class) { + if (classToTransformFrom == Integer.class + || classToTransformFrom == Long.class + || classToTransformFrom == Short.class + || classToTransformFrom == Byte.class) + return true; + } else if (classToTransformTo == Short.class) { + if (classToTransformFrom == Byte.class) + return true; + } else if (classToTransformTo == String.class) { + if (GString.class.isAssignableFrom(classToTransformFrom)) { + return true; + } + } + + return ReflectionCache.isAssignableFrom(classToTransformTo, classToTransformFrom); + } + + public static boolean isGenericSetMethod(MetaMethod method) { + return (method.getName().equals("set")) + && method.getParameterTypes().length == 2; + } + + protected static boolean isSuperclass(Class clazz, Class superclass) { + while (clazz != null) { + if (clazz == superclass) return true; + clazz = clazz.getSuperclass(); + } + return false; + } + + public static boolean parametersAreCompatible(Class[] arguments, Class[] parameters) { + if (arguments.length != parameters.length) return false; + for (int i = 0; i < arguments.length; i++) { + if (!isAssignableFrom(parameters[i], arguments[i])) return false; + } + return true; + } + + public static void logMethodCall(Object object, String methodName, Object[] arguments) { + String className = getClassName(object); + String logname = "methodCalls." + className + "." + methodName; + Logger objLog = Logger.getLogger(logname); + if (!objLog.isLoggable(Level.FINER)) return; + StringBuilder msg = new StringBuilder(methodName); + msg.append("("); + if (arguments != null) { + for (int i = 0; i < arguments.length;) { + msg.append(normalizedValue(arguments[i])); + if (++i < arguments.length) { + msg.append(","); + } + } + } + msg.append(")"); + objLog.logp(Level.FINER, className, msg.toString(), "called from MetaClass.invokeMethod"); + } + + protected static String normalizedValue(Object argument) { + String value; + try { + value = argument.toString(); + if (value.length() > MAX_ARG_LEN) { + value = value.substring(0, MAX_ARG_LEN - 2) + ".."; + } + if (argument instanceof String) { + value = "\'" + value + "\'"; + } + } catch (Exception e) { + value = shortName(argument); + } + return value; + } + + protected static String shortName(Object object) { + if (object == null || object.getClass() == null) return "unknownClass"; + String name = getClassName(object); + if (name == null) return "unknownClassName"; // *very* defensive... + int lastDotPos = name.lastIndexOf('.'); + if (lastDotPos < 0 || lastDotPos >= name.length() - 1) return name; + return name.substring(lastDotPos + 1); + } + + public static Class[] wrap(Class[] classes) { + Class[] wrappedArguments = new Class[classes.length]; + for (int i = 0; i < wrappedArguments.length; i++) { + Class c = classes[i]; + if (c == null) continue; + if (c.isPrimitive()) { + if (c == Integer.TYPE) { + c = Integer.class; + } else if (c == Byte.TYPE) { + c = Byte.class; + } else if (c == Long.TYPE) { + c = Long.class; + } else if (c == Double.TYPE) { + c = Double.class; + } else if (c == Float.TYPE) { + c = Float.class; + } + } else if (isSuperclass(c, GString.class)) { + c = String.class; + } + wrappedArguments[i] = c; + } + return wrappedArguments; + } + + public static boolean sameClasses(Class[] params, Object[] arguments, boolean weakNullCheck) { + if (params.length != arguments.length) + return false; + + for (int i = params.length - 1; i >= 0; i--) { + Object arg = arguments[i]; + Class compareClass = getClassWithNullAndWrapper(arg); + if (params[i] != compareClass) return false; + } + + return true; + } + + private static Class getClassWithNullAndWrapper(Object arg) { + if (arg == null) return null; + if (arg instanceof Wrapper) { + Wrapper w = (Wrapper) arg; + return w.getType(); + } + return arg.getClass(); + } + + public static boolean sameClasses(Class[] params, Object[] arguments) { + if (params.length != arguments.length) + return false; + + for (int i = params.length - 1; i >= 0; i--) { + Object arg = arguments[i]; + if (arg == null) { + if (params[i] != null) + return false; + } else { + if (params[i] != getClassWithNullAndWrapper(arg)) + return false; + } + } + + return true; + } + + public static boolean sameClasses(Class[] params) { + if (params.length != 0) + return false; + + return true; + } + + public static boolean sameClasses(Class[] params, Object arg1) { + if (params.length != 1) + return false; + + if (params[0] != getClassWithNullAndWrapper(arg1)) return false; + + return true; + } + + public static boolean sameClasses(Class[] params, Object arg1, Object arg2) { + if (params.length != 2) + return false; + + if (params[0] != getClassWithNullAndWrapper(arg1)) return false; + if (params[1] != getClassWithNullAndWrapper(arg2)) return false; + + return true; + } + + public static boolean sameClasses(Class[] params, Object arg1, Object arg2, Object arg3) { + if (params.length != 3) + return false; + + if (params[0] != getClassWithNullAndWrapper(arg1)) return false; + if (params[1] != getClassWithNullAndWrapper(arg2)) return false; + if (params[2] != getClassWithNullAndWrapper(arg3)) return false; + + return true; + } + + public static boolean sameClasses(Class[] params, Object arg1, Object arg2, Object arg3, Object arg4) { + if (params.length != 4) + return false; + + if (params[0] != getClassWithNullAndWrapper(arg1)) return false; + if (params[1] != getClassWithNullAndWrapper(arg2)) return false; + if (params[2] != getClassWithNullAndWrapper(arg3)) return false; + if (params[3] != getClassWithNullAndWrapper(arg4)) return false; + + return true; + } + + public static boolean sameClass(Class[] params, Object arg) { + return params[0] == getClassWithNullAndWrapper(arg); + + } + + public static Class[] castArgumentsToClassArray(Object[] argTypes) { + if (argTypes == null) return EMPTY_CLASS_ARRAY; + Class[] classes = new Class[argTypes.length]; + for (int i = 0; i < argTypes.length; i++) { + Object argType = argTypes[i]; + if (argType instanceof Class) { + classes[i] = (Class) argType; + } else if (argType == null) { + classes[i] = null; + } else { +// throw new IllegalArgumentException("Arguments to method [respondsTo] must be of type java.lang.Class!"); + classes[i] = argType.getClass(); + } + } + return classes; + } + + public static void unwrap(Object[] arguments) { + // + // Temp code to ignore wrapped parameters + // The New MOP will deal with these properly + // + for (int i = 0; i != arguments.length; i++) { + if (arguments[i] instanceof Wrapper) { + arguments[i] = ((Wrapper) arguments[i]).unwrap(); + } + } + } + + /** + * Sets the meta class for an object, by delegating to the appropriate + * {@link DefaultGroovyMethods} helper method. This method was introduced as + * a breaking change in 2.0 to solve rare cases of stack overflow. See GROOVY-5285. + * + * The method is named doSetMetaClass in order to prevent misusages. Do not use + * this method directly unless you know what you do. + * + * @param self the object for which to set the meta class + * @param mc the metaclass + */ + public static void doSetMetaClass(Object self, MetaClass mc) { + if (self instanceof GroovyObject) { + DefaultGroovyMethods.setMetaClass((GroovyObject)self, mc); + } else { + DefaultGroovyMethods.setMetaClass(self, mc); + } + } + + /** + * Converts a String into a standard property name. + * + * @param prop the original name + * @return the converted name + */ + public static String convertPropertyName(String prop) { + if (Character.isDigit(prop.charAt(0))) { + return prop; + } + return java.beans.Introspector.decapitalize(prop); + } +}
http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/runtime/MethodClosure.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/runtime/MethodClosure.java b/src/main/java/org/codehaus/groovy/runtime/MethodClosure.java new file mode 100644 index 0000000..7ead0c7 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/runtime/MethodClosure.java @@ -0,0 +1,88 @@ +/* + * 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.Closure; +import groovy.lang.MetaMethod; + +import java.io.IOException; +import java.util.List; + + +/** + * Represents a method on an object using a closure which can be invoked + * at any time + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public class MethodClosure extends Closure { + + public static boolean ALLOW_RESOLVE = false; + + private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + private final String method; + + public MethodClosure(Object owner, String method) { + super(owner); + this.method = method; + + final Class clazz = owner.getClass()==Class.class?(Class) owner:owner.getClass(); + + maximumNumberOfParameters = 0; + parameterTypes = EMPTY_CLASS_ARRAY; + + List<MetaMethod> methods = InvokerHelper.getMetaClass(clazz).respondsTo(owner, method); + + for(MetaMethod m : methods) { + if (m.getParameterTypes().length > maximumNumberOfParameters) { + Class[] pt = m.getNativeParameterTypes(); + maximumNumberOfParameters = pt.length; + parameterTypes = pt; + } + } + } + + public String getMethod() { + return method; + } + + protected Object doCall(Object arguments) { + return InvokerHelper.invokeMethod(getOwner(), method, arguments); + } + + private Object readResolve() { + if (ALLOW_RESOLVE) { + return this; + } + throw new UnsupportedOperationException(); + } + + private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException { + if (ALLOW_RESOLVE) { + stream.defaultReadObject(); + } + throw new UnsupportedOperationException(); + } + + public Object getProperty(String property) { + if ("method".equals(property)) { + return getMethod(); + } else return super.getProperty(property); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/runtime/MethodKey.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/runtime/MethodKey.java b/src/main/java/org/codehaus/groovy/runtime/MethodKey.java new file mode 100644 index 0000000..d03b85a --- /dev/null +++ b/src/main/java/org/codehaus/groovy/runtime/MethodKey.java @@ -0,0 +1,133 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +/** + * An abstract base class for a key used for comparators and Map keys to lookup a method by + * name and parameter types + * + * @author <a href="mailto:[email protected]">James Strachan</a> + */ +public abstract class MethodKey { + + private int hash; + private final String name; + private final Class sender; + private final boolean isCallToSuper; + + public MethodKey(Class sender, String name, boolean isCallToSuper) { + this.sender = sender; + this.name = name; + this.isCallToSuper = isCallToSuper; + } + + /** + * Creates an immutable copy that we can cache. + */ + public MethodKey createCopy() { + int size = getParameterCount(); + Class[] paramTypes = new Class[size]; + for (int i = 0; i < size; i++) { + paramTypes[i] = getParameterType(i); + } + return new DefaultMethodKey(sender, name, paramTypes, isCallToSuper); + } + + public boolean equals(Object that) { + if (this == that) { + return true; + } + else if (that instanceof MethodKey) { + return equals((MethodKey) that); + } + return false; + } + + public boolean equals(MethodKey that) { + int size; + if (sender!=that.sender) return false; + if (isCallToSuper!=that.isCallToSuper) return false; + if (!name.equals(that.name)) return false; + if ((size = getParameterCount()) != that.getParameterCount()) return false; + + for (int i = 0; i < size; i++) { + if (getParameterType(i) != that.getParameterType(i)) { + return false; + } + } + return true; + } + + public int hashCode() { + if (hash == 0) { + hash = createHashCode(); + if (hash == 0) { + hash = 0xcafebabe; + } + } + return hash; + } + + public String toString() { + return super.toString() + "[name:" + name + "; params:" + getParamterTypes(); + } + + public String getName() { + return name; + } + + public List getParamterTypes() { + int size = getParameterCount(); + if (size <= 0) { + return Collections.EMPTY_LIST; + } + List params = new ArrayList(size); + for (int i = 0; i < size; i++) { + params.add(getParameterType(i)); + } + return params; + } + + public abstract int getParameterCount(); + public abstract Class getParameterType(int index); + + protected int createHashCode() { + int answer = name.hashCode(); + int size = getParameterCount(); + + /** @todo we should use the real Josh Bloch algorithm here */ + + // can't remember the exact Josh Bloch algorithm and I've not got the book handy + // but its something like this IIRC + for (int i = 0; i < size; i++) { + answer *= 37; + answer += 1 + getParameterType(i).hashCode(); + } + answer *= 37; + answer += isCallToSuper?1:0; + answer *= 37; + answer += 1 + sender.hashCode(); + return answer; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/runtime/MethodRankHelper.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/runtime/MethodRankHelper.java b/src/main/java/org/codehaus/groovy/runtime/MethodRankHelper.java new file mode 100644 index 0000000..295cbe8 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/runtime/MethodRankHelper.java @@ -0,0 +1,562 @@ +/* + * 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. + * + * @author Hjalmar Ekengren + */ +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[l.size()]); + } + + /** + * 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]; + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/runtime/NullObject.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/runtime/NullObject.java b/src/main/java/org/codehaus/groovy/runtime/NullObject.java new file mode 100644 index 0000000..48f9170 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/runtime/NullObject.java @@ -0,0 +1,176 @@ +/* + * 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.Closure; +import groovy.lang.GroovyObjectSupport; + +import java.util.Collections; +import java.util.Iterator; + +public class NullObject extends GroovyObjectSupport { + private static final NullObject INSTANCE = new NullObject(); + + /** + * private constructor + */ + private NullObject() { + } + + /** + * get the NullObject reference + * + * @return the null object + */ + public static NullObject getNullObject() { + return INSTANCE; + } + + /** + * Since this is implemented as a singleton, we should avoid the + * use of the clone method + */ + public Object clone() { + throw new NullPointerException("Cannot invoke method clone() on null object"); + } + + /** + * Tries to get a property on null, which will always fail + * + * @param property - the property to get + * @return a NPE + */ + public Object getProperty(String property) { + throw new NullPointerException("Cannot get property '" + property + "' on null object"); + } + + /** + * Allows the closure to be called for NullObject + * + * @param closure the closure to call on the object + * @return result of calling the closure + */ + public <T> T with( Closure<T> closure ) { + return DefaultGroovyMethods.with( null, closure ) ; + } + + /** + * Tries to set a property on null, which will always fail + * + * @param property - the proprty to set + * @param newValue - the new value of the property + */ + public void setProperty(String property, Object newValue) { + throw new NullPointerException("Cannot set property '" + property + "' on null object"); + } + + /** + * Tries to invoke a method on null, which will always fail + * + * @param name the name of the method to invoke + * @param args - arguments to the method + * @return a NPE + */ + public Object invokeMethod(String name, Object args) { + throw new NullPointerException("Cannot invoke method " + name + "() on null object"); + } + + /** + * null is only equal to null + * + * @param to - the reference object with which to compare + * @return - true if this object is the same as the to argument + */ + public boolean equals(Object to) { + return to == null; + } + + /** + * iterator() method to be able to iterate on null. + * Note: this part is from Invoker + * + * @return an iterator for an empty list + */ + public Iterator iterator() { + return Collections.EMPTY_LIST.iterator(); + } + + /** + * Allows to add a String to null. + * The result is concatenated String of the result of calling + * toString() on this object and the String in the parameter. + * + * @param s - the String to concatenate + * @return the concatenated string + */ + public Object plus(String s) { + return getMetaClass().invokeMethod(this, "toString", new Object[]{}) + s; + } + + /** + * Fallback for null+null. + * The result is always a NPE. The plus(String) version will catch + * the case of adding a non null String to null. + * + * @param o - the Object + * @return nothing + */ + public Object plus(Object o) { + throw new NullPointerException("Cannot execute null+" + String.valueOf(o)); + } + + /** + * The method "is" is used to test for equal references. + * This method will return true only if the given parameter + * is null + * + * @param other - the object to test + * @return true if other is null + */ + public boolean is(Object other) { + return other == null; + } + + /** + * Type conversion method for null. + * + * @param c - the class to convert to + * @return always null + */ + public Object asType(Class c) { + return null; + } + + /** + * A null object always coerces to false. + * + * @return false + */ + public boolean asBoolean() { + return false; + } + + public String toString() { + return "null"; + } + + public int hashCode() { + throw new NullPointerException("Cannot invoke method hashCode() on null object"); + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a188738d/src/main/java/org/codehaus/groovy/runtime/NumberAwareComparator.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/codehaus/groovy/runtime/NumberAwareComparator.java b/src/main/java/org/codehaus/groovy/runtime/NumberAwareComparator.java new file mode 100644 index 0000000..ee4c8c4 --- /dev/null +++ b/src/main/java/org/codehaus/groovy/runtime/NumberAwareComparator.java @@ -0,0 +1,60 @@ +/* + * 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.GroovyRuntimeException; +import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * Compares two objects using Groovy's friendly comparison algorithm, i.e. + * handles nulls gracefully (nul being less than everything else) and + * performs numeric type coercion if required. + */ +public class NumberAwareComparator<T> implements Comparator<T>, Serializable { + private static final long serialVersionUID = 9017657289076651660L; + + public int compare(T o1, T o2) { + try { + return DefaultTypeTransformation.compareTo(o1, o2); + } catch (ClassCastException cce) { + /* ignore */ + } catch (GroovyRuntimeException gre) { + /* ignore */ + } catch (IllegalArgumentException iae) { + /* ignore */ + } + // since the object does not have a valid compareTo method + // we compare using the hashcodes. null cases are handled by + // DefaultTypeTransformation.compareTo + // This is not exactly a mathematical valid approach, since we compare object + // that cannot be compared. To avoid strange side effects we do a pseudo order + // using hashcodes, but without equality. Since then an x and y with the same + // hashcodes will behave different depending on if we compare x with y or + // x with y, the result might be unstable as well. Setting x and y to equal + // may mean the removal of x or y in a sorting operation, which we don't want. + int x1 = o1.hashCode(); + int x2 = o2.hashCode(); + if (x1 == x2 && o1.equals(o2)) return 0; + if (x1 > x2) return 1; + return -1; + } +}
