AnnotationEqualsTest.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.lang3.external;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;

import org.apache.commons.lang3.AnnotationUtils;
import org.junit.jupiter.api.Test;

/**
 * Regression test for <a href="https://issues.apache.org/jira/browse/LANG-1815">LANG-1815</a>.
 * <p>
 * Verifies that {@link AnnotationUtils} can reflectively read package-private annotation members
 * for {@code equals}, {@code hashCode}, and {@code toString}, and that reflective invocation failures
 * are not treated as inequality.
 * </p>
 *
 * <h2>Important</h2>
 * <p>
 * This test relies on reflective access rules that differ depending on the caller's package.
 * To reproduce the original bug, this class <strong>must remain outside</strong> the
 * {@code org.apache.commons.lang3} package.
 * </p>
 * <p>
 * Do <strong>not</strong> move this class into {@code org.apache.commons.lang3},
 * otherwise the test may no longer exercise the failing scenario from LANG-1815.
 * </p>
 */
public class AnnotationEqualsTest {
    @Retention(RetentionPolicy.RUNTIME)
    @interface Tag {
        String value();
    }

    static class ThrowingTag implements Tag {
        @Override
        public Class<? extends Annotation> annotationType() {
            return Tag.class;
        }

        @Override
        public String value() {
            throw new IllegalArgumentException("boom");
        }
    }

    @Tag("value")
    private final Object a = new Object();
    @Tag("value")
    private final Object b = new Object();

    @Test
    void equalsWorksOnPackagePrivateAnnotations() throws Exception {
        final Tag tagA = getClass().getDeclaredField("a").getAnnotation(Tag.class);
        final Tag tagB = getClass().getDeclaredField("b").getAnnotation(Tag.class);
        assertTrue(AnnotationUtils.equals(tagA, tagB));
    }

    @Test
    void equalsWrapsReflectiveOperationException() throws Exception {
        final Tag tagA = new ThrowingTag();
        final Tag tagB = getClass().getDeclaredField("b").getAnnotation(Tag.class);

        final IllegalStateException ex =
                assertThrows(IllegalStateException.class, () -> AnnotationUtils.equals(tagA, tagB));
        assertInstanceOf(InvocationTargetException.class, ex.getCause());
        assertEquals("boom", ((InvocationTargetException) ex.getCause()).getTargetException().getMessage());
    }

    @Test
    void hashCodeWorksOnPackagePrivateAnnotations() throws Exception {
        final Tag tag = getClass().getDeclaredField("a").getAnnotation(Tag.class);
        assertEquals(tag.hashCode(), AnnotationUtils.hashCode(tag));
    }

    @Test
    void toStringWorksOnPackagePrivateAnnotations() throws Exception {
        final Tag tag = getClass().getDeclaredField("a").getAnnotation(Tag.class);
        final String text = AnnotationUtils.toString(tag);
        assertTrue(text.contains("value=value"), text);
    }

}