Skip to content

Commit

Permalink
#352 Provide a workaround for spring-projects/spring-framework#24029
Browse files Browse the repository at this point in the history
  • Loading branch information
olaf-otto committed Nov 21, 2019
1 parent 55da8b6 commit 0681392
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 2 deletions.
@@ -0,0 +1,94 @@
/*
Copyright 2013 the original author or authors.
Licensed 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
http://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 io.neba.spring.resourcemodels.registration;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

import static java.util.Arrays.asList;
import static java.util.Collections.addAll;

/**
* Supports meta-annotations by looking up annotations in the transitive
* hull (annotations and their annotations, called meta-annotations) of a given
* {@link AnnotatedElement}.
*
* @author Olaf Otto
*/
public class Annotations {
private final AnnotatedElement annotatedElement;
private Map<Class<? extends Annotation>, Annotation> annotations = null;

/**
* @param annotatedElement must not be <code>null</code>
* @return never null.
*/
static Annotations annotations(AnnotatedElement annotatedElement) {
if (annotatedElement == null) {
throw new IllegalArgumentException("Method argument annotatedElement must not be null.");
}
return new Annotations(annotatedElement);
}

/**
* @param annotatedElement must not be <code>null</code>.
*/
private Annotations(AnnotatedElement annotatedElement) {
if (annotatedElement == null) {
throw new IllegalArgumentException("Constructor parameter annotatedElement must not be null.");
}
this.annotatedElement = annotatedElement;
}

/**
* @param type must not be <code>null</code>.
* @return the annotation if present on the given element or any meta-annotation thereof, or <code>null</code>.
*/
@SuppressWarnings("unchecked")
public <T extends Annotation> T get(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Method argument type must not be null.");
}

return (T) getAnnotationMap().get(type);
}

/**
* @return all annotations and meta-annotations present on the element. Never <code>null</code> but rather an empty map.
*/
private Map<Class<? extends Annotation>, Annotation> getAnnotationMap() {
if (this.annotations == null) {
// We do not care about calculating the same thing twice in case of concurrent access.
HashMap<Class<? extends Annotation>, Annotation> annotations = new HashMap<>();
Queue<Annotation> queue = new LinkedList<>(asList(this.annotatedElement.getAnnotations()));
while (!queue.isEmpty()) {
Annotation annotation = queue.remove();
// Prevent lookup loops (@A annotated with @B annotated with @A ...)
if (!annotations.containsKey(annotation.annotationType())) {
annotations.put(annotation.annotationType(), annotation);
addAll(queue, annotation.annotationType().getAnnotations());
}
}
this.annotations = annotations;
}

return this.annotations;
}
}
Expand Up @@ -28,6 +28,7 @@

import javax.annotation.Nonnull;
import javax.annotation.PreDestroy;
import java.lang.annotation.IncompleteAnnotationException;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
Expand Down Expand Up @@ -65,10 +66,18 @@ public void registerModels(BundleContext bundleContext, ConfigurableListableBean
final List<ResourceModelFactory.ModelDefinition> modelDefinitions =
stream(beanNamesForTypeIncludingAncestors(factory, Object.class))
.map(n -> {
final ResourceModel model = factory.findAnnotationOnBean(n, ResourceModel.class);
final Class<?> modelType = factory.getType(n);
if (modelType == null) {
logger.error("The spring application context cannot determine the type of the resource model bean {} in bundle {}. Skipping this model.", n, bundle);
return null;
}

final ResourceModel model = getResourceModelAnnotation(factory, n, modelType);

if (model == null) {
return null;
}

return new ResourceModelFactory.ModelDefinition() {
@Override
@Nonnull
Expand All @@ -88,7 +97,7 @@ public String getName() {
@Override
@Nonnull
public Class<?> getType() {
return factory.getType(n);
return modelType;
}
};
})
Expand Down Expand Up @@ -118,6 +127,17 @@ public Object getModel(@Nonnull ModelDefinition modelDefinition) {
));
}

private ResourceModel getResourceModelAnnotation(ConfigurableListableBeanFactory factory, String n, Class<?> beanType) {
try {
return factory.findAnnotationOnBean(n, ResourceModel.class);
} catch (IncompleteAnnotationException e) {
// Legacy support: This is very likely an old version of the resource model annotation.
// Spring currently assumes it can always load all annotation values, which is not true for
// binary-compatible changes like newly added annotation attributes. This will be fixed in upcoming Spring version (issue 24029).
return Annotations.annotations(beanType).get(ResourceModel.class);
}
}

public void unregister(Bundle bundle) {
ofNullable(this.bundlesWithModels.remove(bundle)).ifPresent(ServiceRegistration::unregister);
}
Expand Down
@@ -0,0 +1,99 @@
/*
Copyright 2013 the original author or authors.
<p>
Licensed 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
<p>
http://www.apache.org/licenses/LICENSE-2.0
<p>
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 io.neba.spring.resourcemodels.registration;

import org.junit.Test;

import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;

import static io.neba.spring.resourcemodels.registration.Annotations.annotations;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.assertj.core.api.Assertions.assertThat;

public class AnnotationsTest {
@TestAnnotation
private static class TestType {
}

@Retention(RUNTIME)
@MetaAnnotation
private @interface TestAnnotation {
}

@Retention(RUNTIME)
@CyclicAnnotation
private @interface MetaAnnotation {
}

@Retention(RUNTIME)
@MetaAnnotation
@CyclicAnnotation
private @interface CyclicAnnotation {
}

private Annotations testee = annotations(TestType.class);

@Test
public void testDetectionOfDirectAnnotation() {
assertAnnotationIsPresent(TestAnnotation.class);
assertAnnotationInstanceCanBeObtained(TestAnnotation.class);
}

@Test
public void testDetectionOfMetaAnnotations() {
assertAnnotationIsPresent(MetaAnnotation.class);
assertAnnotationInstanceCanBeObtained(MetaAnnotation.class);
}

@Test
public void testDetectionOfCyclicMetaAnnotation() {
assertAnnotationIsPresent(CyclicAnnotation.class);
assertAnnotationInstanceCanBeObtained(CyclicAnnotation.class);
}


@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullElementArgumentForLookup() {
this.testee.get(null);
}


@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullTypeArgumentForConstructor() {
annotations(null);
}

@Test
public void testGetAnnotations() {
assertAnnotationsAre(MetaAnnotation.class, CyclicAnnotation.class);
}

@SafeVarargs
private final void assertAnnotationsAre(Class<? extends Annotation>... annotations) {
for (Class<? extends Annotation> annotationType : annotations) {
assertThat(this.testee.get(annotationType)).isNotNull();
}
}

private void assertAnnotationInstanceCanBeObtained(Class<? extends Annotation> type) {
assertThat(this.testee.get(type)).isInstanceOf(type);
}

private void assertAnnotationIsPresent(Class<? extends Annotation> type) {
assertThat(this.testee.get(type)).isNotNull();
}
}
Expand Up @@ -29,14 +29,18 @@
import org.osgi.framework.ServiceRegistration;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

import java.lang.annotation.IncompleteAnnotationException;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -79,6 +83,7 @@ public void mockBundleContext() {
public void testModelRegistration() {
withBeanFactory();
withResourceModelsInApplicationContext("bean1", "bean2");
withResolvableModelType();
registerResourceModels();

assertAllModelsArePublishedViaModelFactory();
Expand Down Expand Up @@ -110,11 +115,47 @@ public void testRegistrarUnregistersAllFactoriesWhenRegistrarStops() {
public void testUserDefinedModelNameOverridesBeanName() {
withBeanFactory();
withResourceModelWithBeanNameAndUserDefinedName("beanName", "userDefinedName");
withResolvableModelType();
registerResourceModels();

assertModelIsPublishedWithName("userDefinedName");
}

@Test
public void testIncompleteAnnotationSupport() {
withBeanFactory();
withResourceModelsInApplicationContext("bean1", "bean2");
withIncompleteResourceModelAnnotation();
withResolvableModelType();
registerResourceModels();

assertAllModelsArePublishedViaModelFactory();
}

@Test
public void testModelBeansWithUnknownTypeAreSkipped() {
withBeanFactory();
withResourceModelsInApplicationContext("bean1", "bean2");
withResolvableModelType();
withUnknownTypeOfBean("bean1");
registerResourceModels();

assertModelIsPublishedWithName("bean2");
}

private void withUnknownTypeOfBean(String name) {
doReturn(null).when(this.factory).getType(name);
}

private void withResolvableModelType() {
doReturn(ModelBean.class).when(this.factory).getType(any());
}

private void withIncompleteResourceModelAnnotation() {
doThrow(new IncompleteAnnotationException(ResourceModel.class, "THIS IS AN EXPECTED TEST EXCEPTION"))
.when(this.factory).findAnnotationOnBean(any(), any());
}

private void assertModelIsPublishedWithName(String name) {
assertThat(this.publishedService.getModelDefinitions()).hasSize(1);
assertThat(this.publishedService.getModelDefinitions().iterator().next().getName()).isEqualTo(name);
Expand Down Expand Up @@ -172,4 +213,9 @@ private void registerResourceModels() {
private void withBeanFactory() {
this.factory = mock(ConfigurableListableBeanFactory.class);
}

@ResourceModel("some/type")
private static class ModelBean {

}
}

0 comments on commit 0681392

Please sign in to comment.