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> -&gt; 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}