001/*
002 * MIT License
003 *
004 * Copyright (c) 2023 Michael Cowan
005 *
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be included in all
014 * copies or substantial portions of the Software.
015 *
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022 * SOFTWARE.
023 */
024
025package io.blt.test.assertj;
026
027import java.lang.annotation.Annotation;
028import java.lang.reflect.Field;
029import java.lang.reflect.Method;
030import java.util.Arrays;
031import java.util.stream.Stream;
032import org.assertj.core.api.ObjectAssert;
033import org.assertj.core.internal.Failures;
034
035import static org.assertj.core.api.Assertions.assertThat;
036
037/**
038 * Assertion factory methods that allow testing the annotations of various types.
039 * <p>
040 * Each method is a static factory for an annotation specific assertion object
041 * i.e. each method returns {@code ObjectAssert<T extends Annotation>}.
042 * </p>
043 * e.g.
044 * <pre>{@code
045 * @Test
046 * void isAnnotatedAsTransactionalWithNoRollbackForException() {
047 *     assertHasAnnotation(NotificationPublisher.class, Transactional.class)
048 *             .extracting(Transactional::noRollbackFor)
049 *             .isEqualTo(Exception.class);
050 * }
051 * }</pre>
052 */
053public final class AnnotationAssertions {
054
055    private AnnotationAssertions() {
056        throw new IllegalAccessError("Utility class should be accessed statically and never constructed");
057    }
058
059    /**
060     * Asserts that a {@code Method} is annotated with a given annotation.
061     * <p>
062     * If present, an assertion object is returned for the found annotation instance, else the test fails.
063     * </p>
064     * e.g.
065     * <pre>{@code
066     * @Test
067     * void isAnnotatedAsTransactionalWithNoRollbackForException() throws Exception {
068     *     var method = NotificationPublisher.class.getMethod("sendNotification");
069     *
070     *     assertHasAnnotation(method, Transactional.class)
071     *             .extracting(Transactional::noRollbackFor)
072     *             .isEqualTo(Exception.class);
073     * }
074     * }</pre>
075     *
076     * @param method     a {@code Method} to test for the presence of {@code annotation}
077     * @param annotation the expected {@code Annotation} type
078     * @param <T>        type of {@code Annotation}
079     * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>}
080     */
081    public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Method method, Class<T> annotation) {
082        return assertThat(findAnnotationOfTypeOrFail(method.getAnnotations(), annotation));
083    }
084
085    /**
086     * Asserts that a {@code Class} is annotated with a given annotation.
087     * <p>
088     * If present, an assertion object is returned for the found annotation instance, else the test fails.
089     * </p>
090     * e.g.
091     * <pre>{@code
092     * @Test
093     * void isAnnotatedAsTransactionalWithNoRollbackForException() {
094     *     assertHasAnnotation(NotificationPublisher.class, Transactional.class)
095     *             .extracting(Transactional::noRollbackFor)
096     *             .isEqualTo(Exception.class);
097     * }
098     * }</pre>
099     *
100     * @param clazz      a {@code Class} to test for the presence of {@code annotation}
101     * @param annotation the expected {@code Annotation} type
102     * @param <T>        type of {@code Annotation}
103     * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>}
104     */
105    public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Class<?> clazz, Class<T> annotation) {
106        return assertThat(findAnnotationOfTypeOrFail(clazz.getAnnotations(), annotation));
107    }
108
109    /**
110     * Asserts that a {@code Field} is annotated with a given annotation.
111     * <p>
112     * If present, an assertion object is returned for the found annotation instance, else the test fails.
113     * </p>
114     * e.g.
115     * <pre>{@code
116     * @Test
117     * void isAnnotatedAsDigitsWithFractionOfTwo() {
118     *     var method = Invoice.class.getField("price");
119     *
120     *     assertHasAnnotation(field, Digits.class)
121     *             .extracting(Digits::fraction)
122     *             .isEqualTo(2);
123     * }
124     * }</pre>
125     *
126     * @param field      a {@code Field} to test for the presence of {@code annotation}
127     * @param annotation the expected {@code Annotation} type
128     * @param <T>        type of {@code Annotation}
129     * @return an assertion object for the found annotation i.e. {@code ObjectAssert<T extends Annotation>}
130     */
131    public static <T extends Annotation> ObjectAssert<T> assertHasAnnotation(Field field, Class<T> annotation) {
132        var annotations = Stream.concat(
133                        Arrays.stream(field.getAnnotations()),
134                        Arrays.stream(field.getAnnotatedType().getAnnotations()))
135                .distinct()
136                .toArray(Annotation[]::new);
137        return assertThat(findAnnotationOfTypeOrFail(annotations, annotation));
138    }
139
140    private static <T extends Annotation> T findAnnotationOfTypeOrFail(Annotation[] annotations, Class<T> type) {
141        var instance = Arrays.stream(annotations)
142                             .filter(a -> a.annotationType().equals(type))
143                             .findFirst()
144                             .orElseThrow(() -> Failures.instance().failure(
145                                     "Cannot find annotation of type " + type.getSimpleName()));
146
147        return type.cast(instance);
148    }
149
150}