diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java index 5e2dfd53..cc0cc7d8 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java @@ -15,20 +15,16 @@ */ package org.springframework.data.jdbc.repository.config; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.function.Function; import java.util.List; import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; import java.util.function.Supplier; -import java.util.stream.Collectors; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -37,6 +33,8 @@ import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBea import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -49,7 +47,8 @@ import org.springframework.util.StringUtils; */ public class JdbcRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { - private ConfigurableListableBeanFactory listableBeanFactory; + private ListableBeanFactory beanFactory; + /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getModuleName() @@ -58,7 +57,6 @@ public class JdbcRepositoryConfigExtension extends RepositoryConfigurationExtens public String getModuleName() { return "JDBC"; } - /* * (non-Javadoc) @@ -82,83 +80,110 @@ public class JdbcRepositoryConfigExtension extends RepositoryConfigurationExtens * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#registerBeansForRoot(org.springframework.beans.factory.support.BeanDefinitionRegistry, org.springframework.data.repository.config.RepositoryConfigurationSource) */ - public void registerBeansForRoot(BeanDefinitionRegistry registry, - RepositoryConfigurationSource configurationSource) { - if (registry instanceof ConfigurableListableBeanFactory) { - this.listableBeanFactory = (ConfigurableListableBeanFactory) registry; + public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) { + + if (registry instanceof ListableBeanFactory) { + this.beanFactory = (ListableBeanFactory) registry; } } - /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) */ @Override public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) { - resolveReference(builder, source, "jdbcOperationsRef", "jdbcOperations", NamedParameterJdbcOperations.class, - true); - resolveReference(builder, source, "dataAccessStrategyRef", "dataAccessStrategy", DataAccessStrategy.class, - false); + + resolveReference(builder, source, "jdbcOperationsRef", "jdbcOperations", NamedParameterJdbcOperations.class, true); + resolveReference(builder, source, "dataAccessStrategyRef", "dataAccessStrategy", DataAccessStrategy.class, false); } private void resolveReference(BeanDefinitionBuilder builder, RepositoryConfigurationSource source, String attributeName, String propertyName, Class classRef, boolean required) { + Optional beanNameRef = source.getAttribute(attributeName).filter(StringUtils::hasText); - String beanName = beanNameRef.orElseGet(() -> { - if (this.listableBeanFactory != null) { - List beanNames = Arrays.asList(listableBeanFactory.getBeanNamesForType(classRef)); - Map bdMap = beanNames.stream() - .collect(Collectors.toMap(Function.identity(), listableBeanFactory::getBeanDefinition)); + String beanName = beanNameRef.orElseGet(() -> determineMatchingBeanName(propertyName, classRef, required)); - if (beanNames.size() > 1) { - // determine primary - - Map primaryBdMap = bdMap.entrySet().stream() - .filter(e -> e.getValue().isPrimary()) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - - Optional primaryBeanName = getSingleBeanName(primaryBdMap.keySet(), classRef, - () -> "more than one 'primary' bean found among candidates: " + primaryBdMap.keySet()); - - // In Java 11 should use Optional.or() - if (primaryBeanName.isPresent()) { - return primaryBeanName.get(); - } - - // determine matchesBeanName - - Optional matchesBeanName = beanNames.stream() - .filter(name -> propertyName.equals(name) - || ObjectUtils.containsElement(listableBeanFactory.getAliases(name), propertyName)) - .findFirst(); - - if (matchesBeanName.isPresent()) { - return matchesBeanName.get(); - } - - } - - if (beanNames.size() == 1) { - return beanNames.get(0); - } - } - return null; - }); if (beanName != null) { builder.addPropertyReference(propertyName, beanName); - } else if (required) { - throw new NoSuchBeanDefinitionException(classRef); - } - } - - private Optional getSingleBeanName(Collection beanNames, Class classRef, - Supplier errorMessage) { - if (beanNames.size() > 1) { - throw new NoUniqueBeanDefinitionException(classRef, beanNames.size(), errorMessage.get()); + } else { + Assert.isTrue(!required, + "The beanName must not be null when requested as 'required'. Please report this as a bug."); } - return beanNames.stream().findFirst(); } + + @Nullable + private String determineMatchingBeanName(String propertyName, Class classRef, boolean required) { + + if (this.beanFactory == null) { + return nullOrThrowException(required, + () -> new NoSuchBeanDefinitionException(classRef, "No BeanFactory available.")); + } + + List beanNames = Arrays.asList(beanFactory.getBeanNamesForType(classRef)); + + if (beanNames.isEmpty()) { + return nullOrThrowException(required, + () -> new NoSuchBeanDefinitionException(classRef, String.format("No bean of type %s available", classRef))); + } + + if (beanNames.size() == 1) { + return beanNames.get(0); + } + + if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { + + return nullOrThrowException(required, + () -> new NoSuchBeanDefinitionException(String.format( + "BeanFactory does not implement ConfigurableListableBeanFactory when trying to find bean of type %s.", + classRef))); + } + + List primaryBeanNames = getPrimaryBeanDefinitions(beanNames, (ConfigurableListableBeanFactory) beanFactory); + + if (primaryBeanNames.size() == 1) { + return primaryBeanNames.get(0); + } + + if (primaryBeanNames.size() > 1) { + throw new NoUniqueBeanDefinitionException(classRef, primaryBeanNames.size(), + "more than one 'primary' bean found among candidates: " + primaryBeanNames); + } + + for (String beanName : beanNames) { + + if (propertyName.equals(beanName) + || ObjectUtils.containsElement(beanFactory.getAliases(beanName), propertyName)) { + return beanName; + } + } + + return nullOrThrowException(required, + () -> new NoSuchBeanDefinitionException(String.format("No bean of name %s found.", propertyName))); + } + + private static List getPrimaryBeanDefinitions(List beanNames, + ConfigurableListableBeanFactory beanFactory) { + + ArrayList primaryBeanNames = new ArrayList<>(); + for (String name : beanNames) { + + if (beanFactory.getBeanDefinition(name).isPrimary()) { + primaryBeanNames.add(name); + } + } + return primaryBeanNames; + } + + @Nullable + private static String nullOrThrowException(boolean required, Supplier exception) { + + if (required) { + throw exception.get(); + } + return null; + } + } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtensionUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtensionUnitTests.java new file mode 100644 index 00000000..cb8052c5 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtensionUnitTests.java @@ -0,0 +1,148 @@ +/* + * 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.jdbc.repository.config; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; + +/** + * @author Jens Schauder + */ +public class JdbcRepositoryConfigExtensionUnitTests { + + BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(); + RepositoryConfigurationSource configSource = mock(RepositoryConfigurationSource.class); + DefaultListableBeanFactory listableBeanFactory = new DefaultListableBeanFactory(); + + JdbcRepositoryConfigExtension extension = new JdbcRepositoryConfigExtension(); + + @Test // DATAJDBC-293 + public void exceptionIsThrownOnPostProcessIfNoBeanFactoryIsAvailable() { + + assertThatThrownBy( // + () -> extension.postProcess(definitionBuilder, configSource)) // + .isInstanceOf(NoSuchBeanDefinitionException.class) // + .hasMessageContaining("No BeanFactory"); + + } + + @Test // DATAJDBC-293 + public void exceptionIsThrownOnPostProcessIfNoJdbcOperationsBeanIsAvailable() { + + extension.registerBeansForRoot(listableBeanFactory, null); + + assertThatThrownBy( // + () -> extension.postProcess(definitionBuilder, configSource)) // + .isInstanceOf(NoSuchBeanDefinitionException.class) // + .hasMessageContaining("NamedParameterJdbcOperations"); // + + } + + @Test // DATAJDBC-293 + public void exceptionIsThrownOnPostProcessIfMultipleJdbcOperationsBeansAreAvailableAndNoConfigurableBeanFactoryAvailable() { + + GenericApplicationContext applicationContext = new GenericApplicationContext(); + + applicationContext.registerBean( // + "one", // + NamedParameterJdbcOperations.class, // + () -> mock(NamedParameterJdbcOperations.class)); + applicationContext.registerBean( // + "two", // + NamedParameterJdbcOperations.class, // + () -> mock(NamedParameterJdbcOperations.class)); + + applicationContext.refresh(); + + extension.registerBeansForRoot(applicationContext, null); + + assertThatThrownBy( // + () -> extension.postProcess(definitionBuilder, configSource)) // + .isInstanceOf(NoSuchBeanDefinitionException.class) // + .hasMessageContaining("NamedParameterJdbcOperations"); // + + } + + @Test // DATAJDBC-293 + public void exceptionIsThrownOnPostProcessIfMultiplePrimaryNoJdbcOperationsBeansAreAvailable() { + + registerJdbcOperations("one", true); + registerJdbcOperations("two", true); + + extension.registerBeansForRoot(listableBeanFactory, null); + + assertThatThrownBy( // + () -> extension.postProcess(definitionBuilder, configSource)) // + .isInstanceOf(NoSuchBeanDefinitionException.class) // + .hasMessageContaining("NamedParameterJdbcOperations"); // + + } + + @Test // DATAJDBC-293 + public void uniquePrimaryBeanIsUsedOfNamedParameterJdbcOperations() { + + registerJdbcOperations("one", false); + registerJdbcOperations("two", true); + + extension.registerBeansForRoot(listableBeanFactory, null); + + extension.postProcess(definitionBuilder, configSource); + + Object jdbcOperations = definitionBuilder.getBeanDefinition().getPropertyValues().get("jdbcOperations"); + + assertThat(jdbcOperations) // + .isInstanceOf(RuntimeBeanReference.class) // + .extracting(rbr -> ((RuntimeBeanReference) rbr).getBeanName()).contains("two"); + + System.out.println(jdbcOperations); + } + + @Test // DATAJDBC-293 + public void matchesByNameAsLastResort() { + + registerJdbcOperations("jdbcOperations", false); + registerJdbcOperations("two", false); + + extension.registerBeansForRoot(listableBeanFactory, null); + + extension.postProcess(definitionBuilder, configSource); + + Object jdbcOperations = definitionBuilder.getBeanDefinition().getPropertyValues().get("jdbcOperations"); + + assertThat(jdbcOperations) // + .isInstanceOf(RuntimeBeanReference.class) // + .extracting(rbr -> ((RuntimeBeanReference) rbr).getBeanName()).contains("jdbcOperations"); + + } + + private void registerJdbcOperations(String name, boolean primary) { + + listableBeanFactory.registerBeanDefinition(name, BeanDefinitionBuilder.genericBeanDefinition( // + NamedParameterJdbcOperations.class, // + () -> mock(NamedParameterJdbcOperations.class)) // + .applyCustomizers(bd -> bd.setPrimary(primary)) // + .getBeanDefinition()); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionTest.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionUnitTests.java similarity index 89% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionTest.java rename to spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionUnitTests.java index 59264e7f..00b5741b 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionTest.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionUnitTests.java @@ -20,9 +20,11 @@ import static org.mockito.Mockito.*; import org.junit.Test; /** + * Unit test for {@link DbActionExecutionException}. + * * @author Jens Schauder */ -public class DbActionExecutionExceptionTest { +public class DbActionExecutionExceptionUnitTests { @Test // DATAJDBC-162 public void constructorWorksWithNullPropertyPath() {