001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.utils; 018 019import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.ObjectUtils.*; 022import static org.apache.juneau.commons.utils.ThrowableUtils.*; 023 024import java.lang.annotation.*; 025import java.lang.reflect.*; 026import java.util.*; 027import java.util.stream.*; 028 029/** 030 * Annotation utilities. 031 */ 032public class AnnotationUtils { 033 034 /** 035 * Prevents instantiation. 036 */ 037 private AnnotationUtils() {} 038 039 /** 040 * Checks if two annotations are equal using the criteria for equality presented in the {@link Annotation#equals(Object)} API docs. 041 * 042 * @param a1 the first Annotation to compare, {@code null} returns {@code false} unless both are {@code null} 043 * @param a2 the second Annotation to compare, {@code null} returns {@code false} unless both are {@code null} 044 * @return {@code true} if the two annotations are {@code equal} or both {@code null} 045 */ 046 public static boolean equals(Annotation a1, Annotation a2) { 047 if (a1 == a2) 048 return true; 049 if (a1 == null || a2 == null) 050 return false; 051 052 var t1 = a1.annotationType(); 053 var t2 = a2.annotationType(); 054 055 if (! t1.equals(t2)) 056 return false; 057 058 return getAnnotationMethods(t1).allMatch(x -> memberEquals(x.getReturnType(), safeSupplier(() -> x.invoke(a1)), safeSupplier(() -> x.invoke(a2)))); 059 } 060 061 /** 062 * Generate a hash code for the given annotation using the algorithm presented in the {@link Annotation#hashCode()} API docs. 063 * 064 * @param a the Annotation for a hash code calculation is desired, not {@code null} 065 * @return the calculated hash code 066 * @throws RuntimeException if an {@code Exception} is encountered during annotation member access 067 * @throws IllegalStateException if an annotation method invocation returns {@code null} 068 */ 069 public static int hash(Annotation a) { 070 return getAnnotationMethods(a.annotationType()).mapToInt(x -> hashMember(x.getName(), safeSupplier(() -> x.invoke(a)))).sum(); 071 } 072 073 /** 074 * Returns a stream of nested annotations in a repeated annotation if the specified annotation is a repeated annotation, 075 * or a singleton stream with the same annotation if not. 076 * 077 * <p> 078 * This method is a stream-based alternative to splitting repeated annotations that avoids creating intermediate arrays. 079 * 080 * <p> 081 * <b>Example:</b> 082 * <p class='bjava'> 083 * <jc>// Given an annotation that may be repeatable</jc> 084 * Annotation <jv>annotation</jv> = ...; 085 * 086 * <jc>// Stream individual annotations (expanded if repeatable)</jc> 087 * streamRepeated(<jv>annotation</jv>) 088 * .forEach(<jv>a</jv> -> System.<jsf>out</jsf>.println(<jv>a</jv>)); 089 * </p> 090 * 091 * @param a The annotation to split. Must not be <jk>null</jk>. 092 * @return A stream of nested annotations, or a singleton stream with the same annotation if it's not repeated. 093 * Never <jk>null</jk>. 094 */ 095 public static Stream<Annotation> streamRepeated(Annotation a) { 096 var ci = info(a.annotationType()); 097 var mi = ci.getRepeatedAnnotationMethod(); 098 if (isNotNull(mi)) { 099 Annotation[] annotations = mi.invoke(a); 100 return Arrays.stream(annotations); 101 } 102 return Stream.of(a); 103 } 104 105 private static boolean annotationArrayMemberEquals(Annotation[] a1, Annotation[] a2) { 106 if (a1.length != a2.length) 107 return false; 108 for (var i = 0; i < a1.length; i++) 109 if (notEqual(a1[i], a2[i])) 110 return false; 111 return true; 112 } 113 114 private static boolean arrayMemberEquals(Class<?> componentType, Object o1, Object o2) { 115 if (componentType.isAnnotation()) 116 return annotationArrayMemberEquals((Annotation[])o1, (Annotation[])o2); 117 if (componentType.equals(Byte.TYPE)) 118 return Arrays.equals((byte[])o1, (byte[])o2); 119 if (componentType.equals(Short.TYPE)) 120 return Arrays.equals((short[])o1, (short[])o2); 121 if (componentType.equals(Integer.TYPE)) 122 return Arrays.equals((int[])o1, (int[])o2); 123 if (componentType.equals(Character.TYPE)) 124 return Arrays.equals((char[])o1, (char[])o2); 125 if (componentType.equals(Long.TYPE)) 126 return Arrays.equals((long[])o1, (long[])o2); 127 if (componentType.equals(Float.TYPE)) 128 return Arrays.equals((float[])o1, (float[])o2); 129 if (componentType.equals(Double.TYPE)) 130 return Arrays.equals((double[])o1, (double[])o2); 131 if (componentType.equals(Boolean.TYPE)) 132 return Arrays.equals((boolean[])o1, (boolean[])o2); 133 return Arrays.equals((Object[])o1, (Object[])o2); 134 } 135 136 private static int arrayMemberHash(Class<?> componentType, Object o) { 137 if (componentType.equals(Byte.TYPE)) 138 return Arrays.hashCode((byte[])o); 139 if (componentType.equals(Short.TYPE)) 140 return Arrays.hashCode((short[])o); 141 if (componentType.equals(Integer.TYPE)) 142 return Arrays.hashCode((int[])o); 143 if (componentType.equals(Character.TYPE)) 144 return Arrays.hashCode((char[])o); 145 if (componentType.equals(Long.TYPE)) 146 return Arrays.hashCode((long[])o); 147 if (componentType.equals(Float.TYPE)) 148 return Arrays.hashCode((float[])o); 149 if (componentType.equals(Double.TYPE)) 150 return Arrays.hashCode((double[])o); 151 if (componentType.equals(Boolean.TYPE)) 152 return Arrays.hashCode((boolean[])o); 153 return Arrays.hashCode((Object[])o); 154 } 155 156 @SuppressWarnings({ 157 "javabugs:S2259" // Class.getDeclaredMethods() never returns null, so fixedSizeList() never returns null here. 158 }) 159 private static Stream<Method> getAnnotationMethods(Class<? extends Annotation> type) { 160 return fixedSizeList(type.getDeclaredMethods()).stream(); 161 } 162 163 private static int hashMember(String name, Object value) { 164 int part1 = name.hashCode() * 127; 165 if (value == null) 166 return part1; 167 if (ClassUtils.isArray(value)) 168 return part1 ^ arrayMemberHash(value.getClass().getComponentType(), value); 169 if (value instanceof Annotation value2) 170 return part1 ^ hash(value2); 171 return part1 ^ value.hashCode(); 172 } 173 174 private static boolean memberEquals(Class<?> type, Object o1, Object o2) { 175 if (o1 == o2) 176 return true; 177 if (o1 == null || o2 == null) 178 return false; 179 if (type.isArray()) 180 return arrayMemberEquals(type.getComponentType(), o1, o2); 181 if (type.isAnnotation()) 182 return equal((Annotation)o1, (Annotation)o2); 183 return o1.equals(o2); 184 } 185}