/* * Copyright (c) 2018, 2019, 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 8212081 8224012 * @summary Test java.lang.Object methods on AnnotatedType objects. */ import java.lang.annotation.*; import java.lang.reflect.*; import java.util.*; import java.util.regex.*; /** * 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); testToString(AnnotatedTypeHost.class); testAnnotationsMatterForEquals(TypeHost.class, AnnotatedTypeHost.class); testGetAnnotations(TypeHost.class, false); testGetAnnotations(AnnotatedTypeHost.class, true); testWildcards(); testFbounds(); 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) { System.err.println("Testing toString on methods of class " + clazz.getName()); Method[] methods = clazz.getDeclaredMethods(); for (Method m : methods) { // Expected information about the type annotations stored // in a *declaration* annotation. AnnotTypeInfo annotTypeInfo = m.getAnnotation(AnnotTypeInfo.class); int expectedAnnotCount = annotTypeInfo.count(); Relation relation = annotTypeInfo.relation(); AnnotatedType annotType = m.getAnnotatedReturnType(); String annotTypeString = annotType.toString(); Type type = m.getGenericReturnType(); String typeString = (type instanceof Class) ? type.getTypeName() : type.toString(); boolean isArray = annotType instanceof AnnotatedArrayType; boolean isVoid = "void".equals(typeString); boolean valid; switch(relation) { case EQUAL: valid = annotTypeString.equals(typeString); break; case POSTFIX: valid = annotTypeString.endsWith(typeString) && !annotTypeString.startsWith(typeString); break; case STRIPPED: String stripped = annotationRegex.matcher(annotTypeString).replaceAll(""); valid = typeString.replace(" ", "").equals(stripped.replace(" ", "")); break; case ARRAY: // 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); break; case OTHER: // No additional checks valid = true; break; default: throw new AssertionError("Shouldn't be reached"); } // Verify number of type annotations matches expected value Matcher matcher = annotationRegex.matcher(annotTypeString); if (expectedAnnotCount > 0) { int i = expectedAnnotCount; int annotCount = 0; while (i > 0) { boolean found = matcher.find(); if (found) { i--; annotCount++; } else { errors++; System.err.println("\tExpected annotation not found: " + annotTypeString); } } } boolean found = matcher.find(); if (found) { errors++; System.err.println("\tAnnotation found unexpectedly: " + annotTypeString); } if (!valid) { errors++; System.err.println(typeString + "\n" + annotTypeString + "\n " + valid + "\n\n"); } } } private static final Pattern annotationRegex = Pattern.compile("@TestObjectMethods\\$AnnotType\\(value=(\\p{Digit})+\\)"); 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 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); } } static void testFbounds() { // Make sure equals and hashCode work fine for a type // involving an F-bound, in particular Comparable in // java.lang.Enum: // // class Enum> // implements Constable, Comparable, Serializable AnnotatedType[] types = Enum.class.getAnnotatedInterfaces(); for (int i = 0; i < types.length; i ++) { for (int j = 0; j < types.length; j ++) { checkTypesForEquality(types[i], types[j], i == j); } } } // 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 { @AnnotTypeInfo public void fooVoid() {return;} @AnnotTypeInfo public int foo() {return 0;} @AnnotTypeInfo public String fooString() {return null;} @AnnotTypeInfo public int[] fooIntArray() {return null;} @AnnotTypeInfo public String[] fooStringArray() {return null;} @AnnotTypeInfo public String [][] fooStringArrayArray() {return null;} @AnnotTypeInfo public Set fooSetString() {return null;} @AnnotTypeInfo public Set fooSetNumber() {return null;} @AnnotTypeInfo public E fooE() {return null;} @AnnotTypeInfo public F fooF() {return null;} @AnnotTypeInfo public G fooG() {return null;} @AnnotTypeInfo public Set fooNumberSet() {return null;} @AnnotTypeInfo public Set fooNumberSet2() {return null;} @AnnotTypeInfo public Set fooNumberSet3() {return null;} @AnnotTypeInfo public Set fooObjectSet() {return null;} @AnnotTypeInfo public List fooObjectList() {return null;} } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE_USE) static @interface AnnotType { int value() default 0; } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) static @interface AnnotTypeInfo { /** * Expected number of @AnnotType */ int count() default 0; /** * Relation to genericString output. */ Relation relation() default Relation.EQUAL; } /** * Expected relationship of toString output of AnnotatedType to * toGenericString output of underlying type. */ static private enum Relation { EQUAL, /** * The toGenericString output is a postfix of the * AnnotatedType output; a leading annotation is expected. */ POSTFIX, /** * If the annotations are stripped from the AnnotatedType * output and whitespace adjusted accordingly, it should equal * the toGenericString output. */ STRIPPED, /** * The output of AnnotatedType for arrays would require more * extensive transformation to map to toGenericString output. */ ARRAY, /** * Some other, harder to characterize, relationship. Currently * used for a wildcard where Object in "extends Object" is * annotated; the "extends Object" is elided in toGenericString. */ OTHER; } static class AnnotatedTypeHost { @AnnotTypeInfo public /*@AnnotType(0)*/ void fooVoid() {return;} // Illegal to annotate void @AnnotTypeInfo(count =1, relation = Relation.POSTFIX) @AnnotType(1) public int foo() {return 0;} @AnnotTypeInfo(count = 1, relation = Relation.POSTFIX) @AnnotType(2) public String fooString() {return null;} @AnnotTypeInfo(count = 1, relation = Relation.ARRAY) public int @AnnotType(3) [] fooIntArray() {return null;} @AnnotTypeInfo(count = 1, relation = Relation.ARRAY) public String @AnnotType(4) [] fooStringArray() {return null;} @AnnotTypeInfo(count = 3, relation = Relation.ARRAY) @AnnotType(5) public String @AnnotType(0) [] @AnnotType(1) [] fooStringArrayArray() {return null;} @AnnotTypeInfo(count = 1, relation = Relation.POSTFIX) @AnnotType(6) public Set fooSetString() {return null;} @AnnotTypeInfo(count = 2, relation = Relation.STRIPPED) @AnnotType(7) public Set<@AnnotType(8) Number> fooSetNumber() {return null;} @AnnotTypeInfo(count = 1, relation = Relation.POSTFIX) @AnnotType(9) public E fooE() {return null;} @AnnotTypeInfo(count = 1, relation = Relation.POSTFIX) @AnnotType(10) public F fooF() {return null;} @AnnotTypeInfo(count = 1, relation = Relation.POSTFIX) @AnnotType(11) public G fooG() {return null;} @AnnotTypeInfo(count = 1, relation = Relation.POSTFIX) @AnnotType(12) public Set fooNumberSet() {return null;} @AnnotTypeInfo(count = 2, relation = Relation.STRIPPED) @AnnotType(13) public Set<@AnnotType(14) ? extends Number> fooNumberSet2() {return null;} @AnnotTypeInfo(count = 2, relation = Relation.STRIPPED) @AnnotType(15) public Set< ? extends @AnnotType(16) Long> fooNumberSet3() {return null;} @AnnotTypeInfo(count = 2, relation = Relation.STRIPPED) @AnnotType(16) public Set<@AnnotType(17) ?> fooObjectSet() {return null;} @AnnotTypeInfo(count = 2, relation = Relation.OTHER) @AnnotType(18) public List fooObjectList() {return null;} } }