vendredi 12 juin 2015

Find closest variable declaration by a concrete instance

for a code generation tool, I want to find a string that represents best a variable declaration.

For example, if I get an empty ArrayList-instance, it is clear that no information about the type is available, so the output should be ArrayList<? extends Object>. However, if I get an ArrayList-instance that contains a String-Element, I assume, that only Strings are inserted and the output should be ArrayList<? extends String.

Here is a small unit test case that shows what the behavior should be:

public class ObjectAnalyserTest {

public ObjectAnalyserTest() {
}

@Test
public void testCreateVariableDeclaration() {
    String o1 = "";
    assertEquals("String", ObjectAnalyser.createVariableDeclaration(o1, true));

    Value<String> v1 = new Value<>(null);
    assertEquals("Value<? extends Object>", ObjectAnalyser.createVariableDeclaration(v1, true));

    Value<String> v2 = new Value<>("");
    assertEquals("Value<? extends String>", ObjectAnalyser.createVariableDeclaration(v2, true));

    Value<Dummy> v3 = new Value<>(new Dummy());
    assertEquals("Value<? extends Dummy>", ObjectAnalyser.createVariableDeclaration(v3, true));

    Pair<? extends Number, ? extends Thread> p1 = new Pair<>(new Integer(5), new Thread(""));
    assertEquals("Pair<? extends Integer, ? extends Thread>", ObjectAnalyser.createVariableDeclaration(p1, true));

    Pair<? extends Number, Dummy2> p2 = new Pair<>(new Integer(5), null);
    assertEquals("Pair<? extends Integer, ? extends Object>", ObjectAnalyser.createVariableDeclaration(p2, true));

    Value2<Integer> v4 = new Value2<>(null);
    assertEquals("Value2<? extends Comparable<? extends Comparable & Serializable> & Serializable>", ObjectAnalyser.createVariableDeclaration(v4, true));

    Value2<Integer> v5 = new Value2<>(1);
    assertEquals("Value2<? extends Integer>", ObjectAnalyser.createVariableDeclaration(v5, true)); // <-- FAILURE

    Array<? extends Object> a1 = new Array<>("Foo");
    assertEquals("Array<? extends String>", ObjectAnalyser.createVariableDeclaration(a1, true));
}

public static class Dummy {}
public static class Dummy2 extends Dummy {}

public static class Value<T> {
    private final T val;

    public Value(T val) {
        this.val = val;
    }

    public T getVal() {
        return val;
    }

}

public static class Value2<V extends Comparable<V> & Serializable> extends Value<V> {
    public Value2(V val) {
        super(val);
    }
}

public static class Pair<A, B> {
    private final A a;
    private final B b;

    public Pair(A a, B b) {
        this.a = a;
        this.b = b;
    }

    public A getA() {
        return a;
    }

    public B getB() {
        return b;
    }

}

public static class Array<T> {
    private final T[] values;

    public Array(T... values) {
        this.values = values;
    }

    public T[] getValues() {
        return values;
    }
}
}

My current implementation goes as follows:

public class ObjectAnalyser {

public static String createVariableDeclaration(Object obj, boolean useSimpleNames) {
    Class<? extends Object> clazz = obj.getClass();
    TypeVariable<? extends Object>[] classTypes = clazz.getTypeParameters();
    if (classTypes.length == 0) {
        //no generic parameter
        if (useSimpleNames) {
            return clazz.getSimpleName();
        } else {
            return clazz.getCanonicalName();
        }
    }
    Type superType = clazz.getGenericSuperclass();
    Type[] interfTypes = clazz.getGenericInterfaces();
    Type[] superTypes = new Type[interfTypes.length + 1];
    superTypes[0] = superType;
    System.arraycopy(interfTypes, 0, superTypes, 1, interfTypes.length);

    StringBuilder str = new StringBuilder(useSimpleNames ? clazz.getSimpleName() : clazz.getCanonicalName());
    str.append("<");
    for (int i=0; i<classTypes.length; ++i) {
        if (i>0) {
            str.append(", ");
        }
        //Search a Method that returns an object of that type
        Class<?> implClass = null;
        for (Method m : clazz.getMethods()) {
            if (m.getReturnType()==Void.TYPE || m.getParameterTypes().length > 0) {
                continue; //no getter
            }
            //TODO: test if this method is declared in this class (clazz.getDeclaredMethods)
            //      if not, use the method from the superclass where it is defined

            Type retType = m.getGenericReturnType();
            Object retObj;
            try {
                retObj = m.invoke(obj);
                implClass = findObject(retObj, classTypes[i], superTypes, retType);
                if (implClass != null) {
                    break;
                }
            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                ex.printStackTrace();
            }
        }
        if (implClass != null) {
            //we found one
            //if (!Modifier.isFinal(implClass.getClass().getModifiers())) {
                str.append("? extends ");
            //}
            str.append(useSimpleNames ? implClass.getSimpleName() : implClass.getCanonicalName());
        } else {
            //use generic upper bound
            Type[] upperBounds = classTypes[i].getBounds();
            assert upperBounds.length > 0;
            str.append("? extends ");
            for (int j=0; j<upperBounds.length; ++j) {
                if (j>0) {
                    str.append(" & ");
                }
                printType(str, upperBounds[j], useSimpleNames, new HashSet<Type>());
            }
        }
    }
    str.append(">");
    return str.toString();
}

private static void printType(StringBuilder str, Type type, boolean useSimpleNames, Set<Type> typeStack) {
    if (type instanceof Class) {
        str.append(useSimpleNames ? ((Class<?>) type).getSimpleName() : ((Class<?>) type).getCanonicalName());
    } else if (type instanceof ParameterizedType) {
        ParameterizedType p = (ParameterizedType) type;
        printType(str, p.getRawType(), useSimpleNames, typeStack);
        if (!typeStack.add(type)) { //to prevent endless loops
            return;
        }
        Type[] actualT = p.getActualTypeArguments();
        if (actualT.length > 0) {
            str.append("<");
            for (int i=0; i<actualT.length; ++i) {
                if (i>0) {
                    str.append(", ");
                }
                printType(str, actualT[i], useSimpleNames, typeStack);
            }
            str.append(">");
        }
        typeStack.remove(type);
    } else if (type instanceof GenericArrayType) {
        //TODO
    } else if (type instanceof WildcardType) {
        //TODO
    } else if (type instanceof TypeVariable) {
        if (typeStack.contains(type)) {
            str.append("?");
            return;
        }
        Type[] upperBounds = ((TypeVariable<?>) type).getBounds();
        assert upperBounds.length > 0;
        str.append("? extends ");
        typeStack.add(type); //to prevent endless loops
        for (int j=0; j<upperBounds.length; ++j) {
            if (j>0) {
                str.append(" & ");
            }
            printType(str, upperBounds[j], useSimpleNames, typeStack);
        }
        typeStack.remove(type);
    }
}

private static Class<? extends Object> findObject(Object methodReturn, TypeVariable<? extends Object> searchedType, Type[] superTypes, Type currentType) {
    if (methodReturn == null) {
        return null;
    }
    if (currentType.equals(searchedType)) {
        //we found the searched type
        //TODO: Does not work if the method comes from the superclass
        return methodReturn.getClass();
    }
    //search in super classes
    for (Type t : superTypes) {
        if (t instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) t;

            for (Type t2 : pt.getActualTypeArguments()) {
                if (t2 instanceof TypeVariable) {
                    TypeVariable<? extends Object> tv = (TypeVariable<? extends Object>) t2;
                    if (tv.getName().equals(searchedType.getName()) && tv.getGenericDeclaration().equals(searchedType.getGenericDeclaration())) {
//                          return methodReturn.getClass(); //we found it
                        //TODO: does not detect that this is also the currentType
                    }
                }
            }
        }
    }

    return null;
}
}

However, the test case fails in the second last statement. I found out that the problem is that when the generic getter, that is used to retrieve the actual type information, comes from a superclass. Then my code does not detect this in the method findObject.

Does anyone knows a way how to handle these inherited methods correctly?

Thank you





Aucun commentaire:

Enregistrer un commentaire