8058202: AnnotatedType implementations don't override toString(), equals(), hashCode()
authordarcy
Wed, 10 Oct 2018 10:28:33 -0700
changeset 52079 5888ef300549
parent 52078 4a63197816ce
child 52080 a2c72b476c9f
8058202: AnnotatedType implementations don't override toString(), equals(), hashCode() Reviewed-by: jfranck
src/java.base/share/classes/sun/reflect/annotation/AnnotatedTypeFactory.java
test/jdk/java/lang/annotation/typeAnnotations/TestObjectMethods.java
--- a/src/java.base/share/classes/sun/reflect/annotation/AnnotatedTypeFactory.java	Wed Oct 10 10:00:25 2018 -0700
+++ b/src/java.base/share/classes/sun/reflect/annotation/AnnotatedTypeFactory.java	Wed Oct 10 10:28:33 2018 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -31,6 +31,8 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
 
 import static sun.reflect.annotation.TypeAnnotation.*;
 
@@ -202,6 +204,68 @@
 
         }
 
+        @Override // java.lang.Object
+        public String toString() {
+            // Reusable toString implementation, but needs to be
+            // specialized for quirks of arrays.
+            return annotationsToString(getAnnotations(), false) + type.toString();
+        }
+
+        protected String annotationsToString(Annotation[] annotations, boolean leadingSpace) {
+            if (annotations != null && annotations.length > 0) {
+                StringJoiner sj = new StringJoiner(" ");
+                if (leadingSpace) {
+                    sj.add(""); // Add a space
+                }
+
+                for (Annotation annotation : annotations) {
+                    sj.add(annotation.toString());
+                }
+
+                if (!leadingSpace) {
+                    sj.add("");
+                }
+                return sj.toString();
+            } else {
+                return "";
+            }
+        }
+
+        protected boolean equalsTypeAndAnnotations(AnnotatedType that) {
+            return getType().equals(that.getType()) &&
+                // Treat ordering of annotations as significant
+                Arrays.equals(getAnnotations(), that.getAnnotations()) &&
+                Objects.equals(getAnnotatedOwnerType(), that.getAnnotatedOwnerType());
+        }
+
+        int baseHashCode() {
+            return type.hashCode() ^
+                // Acceptable to use Objects.hash rather than
+                // Arrays.deepHashCode since the elements of the array
+                // are not themselves arrays.
+                Objects.hash((Object[])getAnnotations()) ^
+                Objects.hash(getAnnotatedOwnerType());
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof AnnotatedType &&
+                !(o instanceof AnnotatedArrayType) &&
+                !(o instanceof AnnotatedTypeVariable) &&
+                !(o instanceof AnnotatedParameterizedType) &&
+                !(o instanceof AnnotatedWildcardType)) {
+                AnnotatedType that = (AnnotatedType) o;
+                return equalsTypeAndAnnotations(that);
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return baseHashCode();
+        }
+
         // Implementation details
         final LocationInfo getLocation() {
             return location;
@@ -244,6 +308,52 @@
             }
             return ((GenericArrayType)t).getGenericComponentType();
         }
+
+        @Override
+        public String toString() {
+            // To annotate the full type of an array, the annotations
+            // are placed between the type and the brackets. For
+            // example, to annotate an array of Strings, the syntax used is
+            //
+            // String @TypeAnnotation []
+            //
+            // and *not*
+            //
+            // @TypeAnnotation String[].
+            //
+            // The toString output should strive to be reusable in
+            // source code. Therefore, the general logic of putting
+            // the annotations before a textual representation of the
+            // type need to be overridden for arrays.
+            StringBuilder sb = new StringBuilder();
+
+            AnnotatedType componentType = this;
+            while (componentType instanceof AnnotatedArrayType) {
+                AnnotatedArrayType annotatedArrayType = (AnnotatedArrayType) componentType;
+                sb.append(annotationsToString(annotatedArrayType.getAnnotations(), true) + "[]");
+                componentType = annotatedArrayType.getAnnotatedGenericComponentType();
+            }
+
+            sb.insert(0, componentType.toString());
+            return sb.toString();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof AnnotatedArrayType) {
+                AnnotatedArrayType that = (AnnotatedArrayType) o;
+                return equalsTypeAndAnnotations(that) &&
+                    Objects.equals(getAnnotatedGenericComponentType(),
+                                   that.getAnnotatedGenericComponentType());
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return baseHashCode() ^ getAnnotatedGenericComponentType().hashCode();
+        }
     }
 
     private static final class AnnotatedTypeVariableImpl extends AnnotatedTypeBaseImpl implements AnnotatedTypeVariable {
@@ -266,6 +376,23 @@
         private TypeVariable<?> getTypeVariable() {
             return (TypeVariable)getType();
         }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof AnnotatedTypeVariable) {
+                AnnotatedTypeVariable that = (AnnotatedTypeVariable) o;
+                return equalsTypeAndAnnotations(that) &&
+                    Arrays.equals(getAnnotatedBounds(), that.getAnnotatedBounds());
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return baseHashCode() ^
+                Objects.hash((Object[])getAnnotatedBounds());
+        }
     }
 
     private static final class AnnotatedParameterizedTypeImpl extends AnnotatedTypeBaseImpl
@@ -316,6 +443,23 @@
         private ParameterizedType getParameterizedType() {
             return (ParameterizedType)getType();
         }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof AnnotatedParameterizedType) {
+                AnnotatedParameterizedType that = (AnnotatedParameterizedType) o;
+                return equalsTypeAndAnnotations(that) &&
+                    Arrays.equals(getAnnotatedActualTypeArguments(), that.getAnnotatedActualTypeArguments());
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return baseHashCode() ^
+                Objects.hash((Object[])getAnnotatedActualTypeArguments());
+        }
     }
 
     private static final class AnnotatedWildcardTypeImpl extends AnnotatedTypeBaseImpl implements AnnotatedWildcardType {
@@ -378,5 +522,26 @@
         private boolean hasUpperBounds() {
             return hasUpperBounds;
         }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof AnnotatedWildcardType) {
+                AnnotatedWildcardType that = (AnnotatedWildcardType) o;
+                return equalsTypeAndAnnotations(that) &&
+                    // Treats ordering as significant
+                    Arrays.equals(getAnnotatedLowerBounds(), that.getAnnotatedLowerBounds()) &&
+                    // Treats ordering as significant
+                    Arrays.equals(getAnnotatedUpperBounds(), that.getAnnotatedUpperBounds());
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public int hashCode() {
+            return baseHashCode() ^
+                Objects.hash((Object[])getAnnotatedLowerBounds()) ^
+                Objects.hash((Object[])getAnnotatedUpperBounds());
+        }
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/lang/annotation/typeAnnotations/TestObjectMethods.java	Wed Oct 10 10:28:33 2018 -0700
@@ -0,0 +1,314 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8058202
+ * @summary Test java.lang.Object methods on AnnotatedType objects.
+ */
+
+import java.lang.annotation.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+/**
+ * Test toString, equals, and hashCode on various AnnotatedType objects.
+ */
+public class TestObjectMethods {
+    private static int errors = 0;
+
+    /*
+     * There are various subtypes of AnnotatedType implementations:
+     *
+     * AnnotatedType
+     * AnnotatedArrayType
+     * AnnotatedParameterizedType
+     * AnnotatedTypeVariable
+     * AnnotatedWildcardType
+     *
+     * The implementations of each these implementations are
+     * examined. Wildcards don't appear as top-level types and need to
+     * be extracted from bounds.
+     *
+     * AnnotatedTypes with and without annotations are examined as
+     * well.
+     */
+    public static void main(String... args) {
+        Class<?>[] testClasses = {TypeHost.class, AnnotatedTypeHost.class};
+
+        for (Class<?> clazz : testClasses) {
+            testEqualsReflexivity(clazz);
+            testEquals(clazz);
+        }
+
+        testToString(TypeHost.class, false);
+        testToString(AnnotatedTypeHost.class, true);
+
+        testAnnotationsMatterForEquals(TypeHost.class, AnnotatedTypeHost.class);
+
+        testGetAnnotations(TypeHost.class, false);
+        testGetAnnotations(AnnotatedTypeHost.class, true);
+
+        testWildcards();
+
+        if (errors > 0) {
+            throw new RuntimeException(errors + " errors");
+        }
+    }
+
+    /*
+     * For non-array types, verify toString version of the annotated
+     * type ends with the same string as the generic type.
+     */
+    static void testToString(Class<?> clazz, boolean leadingAnnotations) {
+        System.err.println("Testing toString on methods of class " + clazz.getName());
+        Method[] methods = clazz.getDeclaredMethods();
+        for (Method m : methods) {
+            AnnotatedType annotType = m.getAnnotatedReturnType();
+            String annotTypeString = annotType.toString();
+
+            Type type = m.getGenericReturnType();
+            String typeString = type.toString();
+
+            boolean isArray = annotType instanceof AnnotatedArrayType;
+            boolean isVoid = "void".equals(typeString);
+
+            boolean valid;
+            if (!isArray) {
+                if (leadingAnnotations && !isVoid) {
+                    valid =
+                        annotTypeString.endsWith(typeString) &&
+                        !annotTypeString.startsWith(typeString);
+                } else {
+                    valid = annotTypeString.equals(typeString);
+                }
+            } else {
+                // Find final non-array component type and gets its name.
+                typeString = null;
+
+                AnnotatedType componentType = annotType;
+                while (componentType instanceof AnnotatedArrayType) {
+                    AnnotatedArrayType annotatedArrayType = (AnnotatedArrayType) componentType;
+                    componentType = annotatedArrayType.getAnnotatedGenericComponentType();
+                }
+
+                String componentName = componentType.getType().getTypeName();
+                valid = annotTypeString.contains(componentName);
+            }
+
+            if (!valid) {
+                errors++;
+                System.err.println(typeString + "\n" + annotTypeString +
+                                   "\n " + valid  +
+                                   "\n\n");
+            }
+        }
+    }
+
+    static void testGetAnnotations(Class<?> clazz, boolean annotationsExpectedOnMethods) {
+        System.err.println("Testing getAnnotations on methods of class " + clazz.getName());
+        Method[] methods = clazz.getDeclaredMethods();
+        for (Method m : methods) {
+            Type type = m.getGenericReturnType();
+            AnnotatedType annotType = m.getAnnotatedReturnType();
+            Annotation[] annotations = annotType.getAnnotations();
+
+            boolean isVoid = "void".equals(type.toString());
+
+            if (annotationsExpectedOnMethods && !isVoid) {
+                if (annotations.length == 0 ) {
+                    errors++;
+                    System.err.println("Expected annotations missing on " + annotType);
+                }
+            } else {
+                if (annotations.length > 0 ) {
+                    errors++;
+                    System.err.println("Unexpected annotations present on " + annotType);
+                }
+            }
+        }
+    }
+
+    static void testEqualsReflexivity(Class<?> clazz) {
+        System.err.println("Testing reflexivity of equals on methods of class " + clazz.getName());
+        Method[] methods = clazz.getDeclaredMethods();
+        for (Method m : methods) {
+            checkTypesForEquality(m.getAnnotatedReturnType(),
+                                  m.getAnnotatedReturnType(),
+                                  true);
+        }
+    }
+
+    private static void checkTypesForEquality(AnnotatedType annotType1,
+                                              AnnotatedType annotType2,
+                                              boolean expected) {
+        boolean comparison = annotType1.equals(annotType2);
+
+        if (comparison) {
+            int hash1 = annotType1.hashCode();
+            int hash2 = annotType2.hashCode();
+            if (hash1 != hash2) {
+                errors++;
+                System.err.format("Equal AnnotatedTypes with unequal hash codes: %n%s%n%s%n",
+                                  annotType1.toString(), annotType2.toString());
+            }
+        }
+
+        if (comparison != expected) {
+            errors++;
+            System.err.println(annotType1);
+            System.err.println(expected ? " is not equal to " : " is equal to ");
+            System.err.println(annotType2);
+            System.err.println();
+        }
+    }
+
+    /*
+     * For each of the type host classes, the return type of a method
+     * should only equal the return type of that method.
+     */
+    static void testEquals(Class<?> clazz) {
+        Method[] methods = clazz.getDeclaredMethods();
+
+        for (int i = 0; i < methods.length; i++) {
+            for (int j = 0; j < methods.length; j++) {
+                if (i == j)
+                    continue;
+                else {
+                    checkTypesForEquality(methods[i].getAnnotatedReturnType(),
+                                          methods[j].getAnnotatedReturnType(),
+                                          false);
+                }
+            }
+        }
+    }
+
+    /**
+     * Roughly, compare the return types of corresponding methods on
+     * TypeHost and AnnotatedtypeHost and verify the AnnotatedType
+     * objects are *not* equal even if their underlying generic types
+     * are.
+     */
+    static void testAnnotationsMatterForEquals(Class<?> clazz1, Class<?> clazz2) {
+        System.err.println("Testing that presence/absence of annotations matters for equals comparison.");
+
+        String methodName = null;
+        for (Method method :  clazz1.getDeclaredMethods()) {
+            if ("void".equals(method.getReturnType().toString())) {
+                continue;
+            }
+
+            methodName = method.getName();
+            try {
+                checkTypesForEquality(method.getAnnotatedReturnType(),
+                                      clazz2.getDeclaredMethod(methodName).getAnnotatedReturnType(),
+                                      false);
+            } catch (Exception e) {
+                errors++;
+                System.err.println("Method " + methodName + " not found.");
+            }
+        }
+    }
+
+
+    static void testWildcards() {
+        System.err.println("Testing wildcards");
+        // public @AnnotType(10) Set<? extends Number> fooNumberSet() {return null;}
+        // public @AnnotType(11) Set<@AnnotType(13) ? extends Number> fooNumberSet2() {return null;}
+        AnnotatedWildcardType awt1 = extractWildcard("fooNumberSet");
+        AnnotatedWildcardType awt2 = extractWildcard("fooNumberSet2");
+
+        if (!awt1.equals(extractWildcard("fooNumberSet")) ||
+            !awt2.equals(extractWildcard("fooNumberSet2"))) {
+            errors++;
+            System.err.println("Bad equality comparison on wildcards.");
+        }
+
+        checkTypesForEquality(awt1, awt2, false);
+
+        if (awt2.getAnnotations().length == 0) {
+            errors++;
+            System.err.println("Expected annotations not found.");
+        }
+    }
+
+    private static AnnotatedWildcardType extractWildcard(String methodName) {
+        try {
+            return (AnnotatedWildcardType)
+                (((AnnotatedParameterizedType)(AnnotatedTypeHost.class.getMethod(methodName).
+                                               getAnnotatedReturnType())).
+                 getAnnotatedActualTypeArguments()[0] );
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // The TypeHost and AnnotatedTypeHost classes declare methods with
+    // the same name and signatures but with the AnnotatedTypeHost
+    // methods having annotations on their return type, where
+    // possible.
+
+    static class TypeHost<E, F extends Number> {
+        public void fooVoid() {return;}
+
+        public int foo() {return 0;}
+        public String fooString() {return null;}
+
+        public int[] fooIntArray() {return null;}
+        public String[] fooStringArray() {return null;}
+        public String [][] fooStringArrayArray() {return null;}
+
+        public Set<String> fooSetString() {return null;}
+        public E fooE() {return null;}
+        public F fooF() {return null;}
+        public <G> G fooG() {return null;}
+
+        public  Set<? extends Number> fooNumberSet() {return null;}
+        public  Set<? extends Integer> fooNumberSet2() {return null;}
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.TYPE_USE)
+    static @interface AnnotType {
+        int value() default 0;
+    }
+
+    static class AnnotatedTypeHost<E, F extends Number> {
+        public /*@AnnotType(0)*/ void fooVoid() {return;} // Illegal to annotate void
+
+        public @AnnotType(1) int foo() {return 0;}
+        public @AnnotType(2) String fooString() {return null;}
+
+        public  int @AnnotType(3) [] fooIntArray() {return null;}
+        public  String @AnnotType(4) [] fooStringArray() {return null;}
+        public  @AnnotType(5) String  @AnnotType(0) [] @AnnotType(1) [] fooStringArrayArray() {return null;}
+
+        public @AnnotType(6) Set<String> fooSetString() {return null;}
+        public @AnnotType(7) E fooE() {return null;}
+        public @AnnotType(8) F fooF() {return null;}
+        public @AnnotType(9) <G> G fooG() {return null;}
+
+        public @AnnotType(10) Set<? extends Number> fooNumberSet() {return null;}
+        public @AnnotType(11) Set<@AnnotType(13) ? extends Number> fooNumberSet2() {return null;}
+    }
+}