8058202: AnnotatedType implementations don't override toString(), equals(), hashCode()
Reviewed-by: jfranck
--- 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;}
+ }
+}