diff --git a/src/main/java/org/springframework/data/util/BeanLookup.java b/src/main/java/org/springframework/data/util/BeanLookup.java new file mode 100644 index 000000000..fb27687e0 --- /dev/null +++ b/src/main/java/org/springframework/data/util/BeanLookup.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 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 org.springframework.data.util; + +import lombok.experimental.UtilityClass; + +import java.util.Map; + +import javax.annotation.Nullable; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.util.Assert; + +/** + * Simple helper to allow lenient lookup of beans of a given type from a {@link ListableBeanFactory}. This is not user + * facing API but a mere helper for Spring Data configuration code. + * + * @author Oliver Gierke + * @since 2.1 + * @soundtrack Dave Matthews Band - Bartender (DMB Live 25) + */ +@UtilityClass +public class BeanLookup { + + /** + * Returns a {@link Lazy} for the unique bean of the given type from the given {@link BeanFactory} (which needs to be + * a {@link ListableBeanFactory}). The lookup will produce a {@link NoUniqueBeanDefinitionException} in case multiple + * beans of the given type are available in the given {@link BeanFactory}. + * + * @param type must not be {@literal null}. + * @param beanFactory the {@link BeanFactory} to lookup the bean from. + * @return a {@link Lazy} for the unique bean of the given type or the instance provided by the fallback in case no + * bean of the given type can be found. + */ + public static Lazy lazyIfAvailable(Class type, BeanFactory beanFactory) { + + Assert.notNull(type, "Type must not be null!"); + Assert.isInstanceOf(ListableBeanFactory.class, beanFactory); + + return Lazy.of(() -> lookupBean(type, (ListableBeanFactory) beanFactory)); + } + + /** + * Looks up the unique bean of the given type from the given {@link ListableBeanFactory}. + * + * @param type must not be {@literal null}. + * @param beanFactory must not be {@literal null}. + * @return + */ + @Nullable + private static T lookupBean(Class type, ListableBeanFactory beanFactory) { + + Map names = beanFactory.getBeansOfType(type, false, false); + + switch (names.size()) { + + case 0: + return null; + case 1: + return names.values().iterator().next(); + default: + throw new NoUniqueBeanDefinitionException(type, names.keySet()); + } + } +} diff --git a/src/main/java/org/springframework/data/web/config/QuerydslWebConfiguration.java b/src/main/java/org/springframework/data/web/config/QuerydslWebConfiguration.java index 92fa995b2..ff34420eb 100644 --- a/src/main/java/org/springframework/data/web/config/QuerydslWebConfiguration.java +++ b/src/main/java/org/springframework/data/web/config/QuerydslWebConfiguration.java @@ -19,12 +19,14 @@ import java.util.List; import java.util.Optional; import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.core.convert.ConversionService; +import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; import org.springframework.data.web.querydsl.QuerydslPredicateArgumentResolver; @@ -45,6 +47,7 @@ import com.querydsl.core.types.Predicate; public class QuerydslWebConfiguration implements WebMvcConfigurer { @Autowired @Qualifier("mvcConversionService") ObjectFactory conversionService; + @Autowired ObjectProvider resolver; /** * Default {@link QuerydslPredicateArgumentResolver} to create Querydsl {@link Predicate} instances for Spring MVC @@ -61,7 +64,7 @@ public class QuerydslWebConfiguration implements WebMvcConfigurer { @Lazy @Bean public QuerydslBindingsFactory querydslBindingsFactory() { - return new QuerydslBindingsFactory(SimpleEntityPathResolver.INSTANCE); + return new QuerydslBindingsFactory(resolver.getIfUnique(() -> SimpleEntityPathResolver.INSTANCE)); } /* diff --git a/src/test/java/org/springframework/data/util/BeanLookupUnitTests.java b/src/test/java/org/springframework/data/util/BeanLookupUnitTests.java new file mode 100644 index 000000000..f2e3ffa5f --- /dev/null +++ b/src/test/java/org/springframework/data/util/BeanLookupUnitTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 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 org.springframework.data.util; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.querydsl.SimpleEntityPathResolver; + +/** + * Unit tests for {@link BeanLookup}. + * + * @author Oliver Gierke + * @soundtrack Dave Matthews Band - Shotgun (DMB Live 25) + */ +@RunWith(MockitoJUnitRunner.class) +public class BeanLookupUnitTests { + + @Mock ListableBeanFactory beanFactory; + Map beans; + + @Before + public void setUp() { + + this.beans = new HashMap<>(); + + doReturn(beans).when(beanFactory).getBeansOfType(EntityPathResolver.class, false, false); + } + + @Test // DATACMNS-1235 + public void returnsUniqueBeanByType() { + + beans.put("foo", SimpleEntityPathResolver.INSTANCE); + + assertThat(BeanLookup.lazyIfAvailable(EntityPathResolver.class, beanFactory).get()) // + .isEqualTo(SimpleEntityPathResolver.INSTANCE); + } + + @Test // DATACMNS-1235 + public void returnsEmptyLazyIfNoBeanAvailable() { + assertThat(BeanLookup.lazyIfAvailable(EntityPathResolver.class, beanFactory).getOptional()).isEmpty(); + } + + @Test // DATACMNS-1235 + public void throwsExceptionIfMultipleBeansAreAvailable() { + + beans.put("foo", SimpleEntityPathResolver.INSTANCE); + beans.put("bar", SimpleEntityPathResolver.INSTANCE); + + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) // + .isThrownBy(() -> BeanLookup.lazyIfAvailable(EntityPathResolver.class, beanFactory).get()) // + .withMessageContaining("foo") // + .withMessageContaining("bar") // + .withMessageContaining(EntityPathResolver.class.getName()); + } +} diff --git a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java index ee3e68c08..26db02ead 100755 --- a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java +++ b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java @@ -30,6 +30,9 @@ import org.springframework.core.convert.ConversionService; import org.springframework.data.classloadersupport.HidingClassLoader; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.querydsl.binding.QuerydslBindingsFactory; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.data.web.PagedResourcesAssemblerArgumentResolver; import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver; @@ -88,6 +91,18 @@ public class EnableSpringDataWebSupportIntegrationTests { } } + @Configuration + @EnableSpringDataWebSupport + static class CustomEntityPathResolver { + + static SimpleEntityPathResolver resolver = new SimpleEntityPathResolver("suffix"); + + @Bean + SimpleEntityPathResolver entityPathResolver() { + return resolver; + } + } + @Test // DATACMNS-330 public void registersBasicBeanDefinitions() throws Exception { @@ -215,6 +230,16 @@ public class EnableSpringDataWebSupportIntegrationTests { assertThat(adapter.getArgumentResolvers().get(0)).isInstanceOf(ProxyingHandlerMethodArgumentResolver.class); } + @Test // DATACMNS-1235 + public void picksUpEntityPathResolverIfRegistered() { + + WebApplicationContext context = WebTestUtils.createApplicationContext(CustomEntityPathResolver.class); + + assertThat(context.getBean(EntityPathResolver.class)).isEqualTo(CustomEntityPathResolver.resolver); + assertThat(context.getBean(QuerydslBindingsFactory.class).getEntityPathResolver()) + .isEqualTo(CustomEntityPathResolver.resolver); + } + private static void assertResolversRegistered(ApplicationContext context, Class... resolverTypes) { RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);