diff --git a/src/main/java/org/springframework/data/repository/init/ResourceReaderRepositoryPopulator.java b/src/main/java/org/springframework/data/repository/init/ResourceReaderRepositoryPopulator.java index e962e37b9..ef27fd1e6 100644 --- a/src/main/java/org/springframework/data/repository/init/ResourceReaderRepositoryPopulator.java +++ b/src/main/java/org/springframework/data/repository/init/ResourceReaderRepositoryPopulator.java @@ -15,35 +15,49 @@ */ package org.springframework.data.repository.init; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import java.io.IOException; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; -import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultCrudMethods; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.data.repository.support.Repositories; -import org.springframework.data.repository.support.RepositoryInvoker; -import org.springframework.data.repository.support.RepositoryInvokerFactory; +import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; /** * A {@link RepositoryPopulator} using a {@link ResourceReader} to read objects from the configured {@link Resource}s. * * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch * @since 1.4 */ public class ResourceReaderRepositoryPopulator implements RepositoryPopulator, ApplicationEventPublisherAware { - private static final Log logger = LogFactory.getLog(ResourceReaderRepositoryPopulator.class); + private static final Log logger = LogFactory.getLog(ResourceReaderRepositoryPopulator.class); private final ResourceReader reader; private final @Nullable ClassLoader classLoader; @@ -114,7 +128,7 @@ public class ResourceReaderRepositoryPopulator implements RepositoryPopulator, A Assert.notNull(repositories, "Repositories must not be null!"); - RepositoryInvokerFactory invokerFactory = new DefaultRepositoryInvokerFactory(repositories); + RepositoryPersisterFactory persisterFactory = new RepositoryPersisterFactory(repositories); for (Resource resource : resources) { @@ -125,13 +139,13 @@ public class ResourceReaderRepositoryPopulator implements RepositoryPopulator, A if (result instanceof Collection) { for (Object element : (Collection) result) { if (element != null) { - persist(element, invokerFactory); + persist(element, persisterFactory); } else { logger.info("Skipping null element found in unmarshal result!"); } } } else { - persist(result, invokerFactory); + persist(result, persisterFactory); } } @@ -158,12 +172,172 @@ public class ResourceReaderRepositoryPopulator implements RepositoryPopulator, A * Persists the given {@link Object} using a suitable repository. * * @param object must not be {@literal null}. - * @param invokerFactory must not be {@literal null}. + * @param persisterFactory must not be {@literal null}. */ - private void persist(Object object, RepositoryInvokerFactory invokerFactory) { + private void persist(Object object, RepositoryPersisterFactory persisterFactory) { - RepositoryInvoker invoker = invokerFactory.getInvokerFor(object.getClass()); - logger.debug(String.format("Persisting %s using repository %s", object, invoker)); - invoker.invokeSave(object); + RepositoryPersister persister = persisterFactory.getPersisterFor(object.getClass()); + logger.debug(String.format("Persisting %s using repository %s", object, persister)); + persister.save(object); + } + + /** + * Factory to create {@link RepositoryPersister} instances. + */ + static class RepositoryPersisterFactory { + + private final Map, RepositoryPersister> persisters = new HashMap<>(); + private final Repositories repositories; + + public RepositoryPersisterFactory(Repositories repositories) { + this.repositories = repositories; + } + + /** + * Obtain a {@link RepositoryPersister}. + * + * @param domainType + * @return + */ + public RepositoryPersister getPersisterFor(Class domainType) { + return persisters.computeIfAbsent(domainType, this::createPersisterFor); + } + + private RepositoryPersister createPersisterFor(Class domainType) { + + RepositoryInformation repositoryInformation = repositories.getRequiredRepositoryInformation(domainType); + Object repository = repositories.getRepositoryFor(domainType).orElseThrow( + () -> new IllegalArgumentException(String.format("No repository found for domain type: %s", domainType))); + + if (repositoryInformation.isReactiveRepository()) { + return repository instanceof ReactiveCrudRepository ? new ReactiveCrudRepositoryPersister(repository) + : new ReflectiveReactivePersister(repositoryInformation, repository); + } + + if (repository instanceof CrudRepository) { + return new CrudRepositoryPersister(repository); + } + + return new ReflectivePersister(repositoryInformation, repository); + } + } + + /** + * Interface defining a save method to persist an object within a repository. + */ + interface RepositoryPersister { + + /** + * Saves the {@code object} in an appropriate repository. + * + * @param object + */ + void save(Object object); + } + + /** + * Reflection variant of a {@link RepositoryPersister}. + */ + private static class ReflectivePersister implements RepositoryPersister { + + private final CrudMethods methods; + private final Object repository; + + public ReflectivePersister(RepositoryMetadata metadata, Object repository) { + this.methods = new DefaultCrudMethods(metadata); + this.repository = repository; + } + + @Override + public void save(Object object) { + + doPersist(object); + } + + Object doPersist(Object object) { + Method method = methods.getSaveMethod()// + .orElseThrow(() -> new IllegalStateException("Repository doesn't have a save-method declared!")); + + return ReflectionUtils.invokeMethod(method, repository, object); + } + + @Override + public String toString() { + return repository.toString(); + } + } + + /** + * Reactive extension to save objects in a reactive repository. + */ + private static class ReflectiveReactivePersister extends ReflectivePersister { + + public ReflectiveReactivePersister(RepositoryMetadata metadata, Object repository) { + super(metadata, repository); + } + + @Override + public void save(Object object) { + + Object wrapper = doPersist(object); + + Publisher publisher = ReactiveWrapperConverters.toWrapper(wrapper, Publisher.class); + + if (!(publisher instanceof Mono)) { + publisher = Flux.from(publisher).collectList(); + } + + Mono.from(publisher).block(); + } + } + + /** + * {@link RepositoryPersister} to operate with {@link CrudRepository}. + */ + private static class CrudRepositoryPersister implements RepositoryPersister { + + private final CrudRepository repository; + + @SuppressWarnings("unchecked") + public CrudRepositoryPersister(Object repository) { + + Assert.isInstanceOf(CrudRepository.class, repository); + this.repository = (CrudRepository) repository; + } + + @Override + public void save(Object object) { + repository.save(object); + } + + @Override + public String toString() { + return repository.toString(); + } + } + + /** + * {@link RepositoryPersister} to operate with {@link ReactiveCrudRepository}. + */ + private static class ReactiveCrudRepositoryPersister implements RepositoryPersister { + + private final ReactiveCrudRepository repository; + + @SuppressWarnings("unchecked") + public ReactiveCrudRepositoryPersister(Object repository) { + + Assert.isInstanceOf(ReactiveCrudRepository.class, repository); + this.repository = (ReactiveCrudRepository) repository; + } + + @Override + public void save(Object object) { + repository.save(object).block(); + } + + @Override + public String toString() { + return repository.toString(); + } } } diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java index 6ef6bb3c1..7714509e1 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java +++ b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java @@ -17,15 +17,13 @@ package org.springframework.data.repository.core.support; import static org.mockito.Mockito.*; -import java.io.Serializable; - import org.springframework.data.mapping.context.SampleMappingContext; import org.springframework.data.repository.Repository; /** * @author Oliver Gierke */ -public class DummyRepositoryFactoryBean, S, ID extends Serializable> +public class DummyRepositoryFactoryBean, S, ID> extends RepositoryFactoryBeanSupport { private final T repository; diff --git a/src/test/java/org/springframework/data/repository/init/ReactiveResourceReaderRepositoryPopulatorUnitTests.java b/src/test/java/org/springframework/data/repository/init/ReactiveResourceReaderRepositoryPopulatorUnitTests.java new file mode 100755 index 000000000..f2a83eece --- /dev/null +++ b/src/test/java/org/springframework/data/repository/init/ReactiveResourceReaderRepositoryPopulatorUnitTests.java @@ -0,0 +1,206 @@ +/* + * Copyright 2012-2022 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 + * + * 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.springframework.data.repository.init; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import io.reactivex.rxjava3.core.Single; +import reactor.core.publisher.Mono; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DummyRepositoryFactoryBean; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.data.repository.reactive.RxJava3CrudRepository; +import org.springframework.data.repository.support.Repositories; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Unit tests for {@link ResourceReaderRepositoryPopulator} using reactive repositories. + * + * @author Mark Paluch + */ +@SpringJUnitConfig(classes = ReactiveResourceReaderRepositoryPopulatorUnitTests.ReactiveSampleConfiguration.class) +class ReactiveResourceReaderRepositoryPopulatorUnitTests { + + @Autowired ReactivePersonRepository personRepository; + @Autowired ReactiveContactRepository contactRepository; + @Autowired RxJavaUserRepository userRepository; + @Autowired Repositories repositories; + + ApplicationEventPublisher publisher; + ResourceReader reader; + Resource resource; + + @BeforeEach + void setUp() { + + this.reader = mock(ResourceReader.class); + this.publisher = mock(ApplicationEventPublisher.class); + this.resource = mock(Resource.class); + } + + @Test + void storesSingleUsingReactiveRepositoryObjectCorrectly() throws Exception { + + ReactivePerson reference = new ReactivePerson(); + when(personRepository.save(reference)).thenReturn(Mono.just(reference)); + setUpReferenceAndInitialize(reference); + + verify(personRepository).save(reference); + } + + @Test + void storesSingleUsingSimpleReactiveRepositoryObjectCorrectly() throws Exception { + + ReactiveContact reference = new ReactiveContact(); + when(contactRepository.save(reference)).thenReturn(Mono.just(reference)); + setUpReferenceAndInitialize(reference); + + verify(contactRepository).save(reference); + } + + @Test + void storesSingleUsingRxJavaRepositoryObjectCorrectly() throws Exception { + + ReactiveUser reference = new ReactiveUser(); + when(userRepository.save(reference)).thenReturn(Single.just(reference)); + setUpReferenceAndInitialize(reference); + + verify(userRepository).save(reference); + } + + @Test + void emitsRepositoriesPopulatedEventIfPublisherConfigured() throws Exception { + + ReactivePerson reference = new ReactivePerson(); + when(personRepository.save(reference)).thenReturn(Mono.just(reference)); + RepositoryPopulator populator = setUpReferenceAndInitialize(reference, publisher); + + ApplicationEvent event = new RepositoriesPopulatedEvent(populator, repositories); + verify(publisher, times(1)).publishEvent(event); + } + + private RepositoryPopulator setUpReferenceAndInitialize(Object reference, ApplicationEventPublisher publish) + throws Exception { + + when(reader.readFrom(any(), any())).thenReturn(reference); + + ResourceReaderRepositoryPopulator populator = new ResourceReaderRepositoryPopulator(reader); + populator.setResources(resource); + populator.setApplicationEventPublisher(publisher); + populator.populate(repositories); + + return populator; + } + + private RepositoryPopulator setUpReferenceAndInitialize(Object reference) throws Exception { + return setUpReferenceAndInitialize(reference, null); + } + + @Configuration + static class ReactiveSampleConfiguration { + + @Autowired ApplicationContext context; + + @Bean + Repositories repositories() { + return new Repositories(context); + } + + @Bean + ReactivePersonRepository personRepository() { + return mock(ReactivePersonRepository.class); + } + + @Bean + RepositoryFactoryBeanSupport, ReactivePerson, Object> personRepositoryFactory( + ReactivePersonRepository personRepository) { + + DummyRepositoryFactoryBean, ReactivePerson, Object> factoryBean = new DummyRepositoryFactoryBean<>( + ReactivePersonRepository.class); + factoryBean.setCustomImplementation(personRepository); + return factoryBean; + } + + @Bean + ReactiveContactRepository contactRepository() { + return mock(ReactiveContactRepository.class); + } + + @Bean + RepositoryFactoryBeanSupport, ReactiveContact, Object> contactRepositoryFactory( + ReactiveContactRepository contactRepository) { + + DummyRepositoryFactoryBean, ReactiveContact, Object> factoryBean = new DummyRepositoryFactoryBean<>( + ReactiveContactRepository.class); + factoryBean.setCustomImplementation(contactRepository); + return factoryBean; + } + + @Bean + RxJavaUserRepository userRepository() { + return mock(RxJavaUserRepository.class); + } + + @Bean + RepositoryFactoryBeanSupport, ReactiveUser, Object> userRepositoryFactory( + RxJavaUserRepository userRepository) { + + DummyRepositoryFactoryBean, ReactiveUser, Object> factoryBean = new DummyRepositoryFactoryBean<>( + RxJavaUserRepository.class); + factoryBean.setCustomImplementation(userRepository); + return factoryBean; + } + } + + static class ReactivePerson { + + } + + static class ReactiveContact { + + } + + static class ReactiveUser { + + } + + interface ReactivePersonRepository extends ReactiveCrudRepository { + + } + + interface ReactiveContactRepository extends Repository { + + Mono save(ReactiveContact contact); + + } + + interface RxJavaUserRepository extends RxJava3CrudRepository { + + } +}