MapRepositoriesConfigurationExtensionIntegrationTests.java

/*
 * Copyright 2014-2025 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.map.repository.config;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.function.Predicate;

import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort;
import org.springframework.data.keyvalue.core.KeyValueAdapter;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.data.keyvalue.core.PathSortAccessor;
import org.springframework.data.keyvalue.core.QueryEngine;
import org.springframework.data.keyvalue.core.QueryEngineFactory;
import org.springframework.data.keyvalue.core.SortAccessor;
import org.springframework.data.keyvalue.core.SpelQueryEngine;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.data.keyvalue.repository.KeyValueRepository;
import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator;
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean;
import org.springframework.data.map.MapKeyValueAdapter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.test.util.ReflectionTestUtils;

/**
 * Integration tests for {@link MapRepositoryConfigurationExtension}.
 *
 * @author Oliver Gierke
 * @author Christoph Strobl
 * @author Mark Paluch
 */
class MapRepositoriesConfigurationExtensionIntegrationTests {

	@Test // DATAKV-86
	void registersDefaultTemplateIfReferenceNotCustomized() {

		ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class);

		assertThat(Arrays.asList(context.getBeanDefinitionNames())).contains("mapKeyValueTemplate");

		context.close();
	}

	@Test // DATAKV-86
	void doesNotRegisterDefaultTemplateIfReferenceIsCustomized() {

		ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
				ConfigWithCustomTemplateReference.class);

		assertThat(context.getBeanDefinitionNames()).doesNotContain("mapKeyValueTemplate");

		context.close();
	}

	@Test // GH-358
	void shouldUseCustomAdapter() {

		ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
				ConfigWithOverriddenTemplateReference.class);

		PersonRepository repository = context.getBean(PersonRepository.class);

		assertThatThrownBy(() -> repository.findById("foo")).hasRootCauseInstanceOf(IllegalStateException.class)
				.hasMessageContaining("Mock");

		context.close();
	}

	@Test // DATAKV-87
	void considersMapTypeConfiguredOnAnnotation() {
		assertKeyValueTemplateWithAdapterFor(ConcurrentSkipListMap.class,
				new AnnotationConfigApplicationContext(ConfigWithCustomizedMapType.class));
	}

	@Test // DATAKV-87
	void doesNotConsiderMapConfiguredIfTemplateIsPresent() {
		assertKeyValueTemplateWithAdapterFor(ConcurrentHashMap.class, new AnnotationConfigApplicationContext(
				ConfigWithCustomizedMapTypeAndExplicitDefinitionOfKeyValueTemplate.class));
	}

	@Test // GH-565
	void considersSortAccessorConfiguredOnAnnotation() {
		assertKeyValueTemplateWithSortAccessorFor(PathSortAccessor.class,
				new AnnotationConfigApplicationContext(ConfigWithCustomizedSortAccessor.class));
	}

	@Test // GH-576
	void considersQueryEngineConfiguration() {

		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithQueryEngine.class);

		KeyValueTemplate template = context.getBean(KeyValueTemplate.class);
		Object adapter = ReflectionTestUtils.getField(template, "adapter");

		assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class);

		Object engine = ReflectionTestUtils.getField(adapter, "engine");

		assertThat(engine).isInstanceOf(SpelQueryEngine.class);
	}

	@Test // GH-576
	void considersQueryEngineAndSortAccessorConfiguration() {

		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
				ConfigWithQueryEngineAndCustomizedSortAccessor.class);

		KeyValueTemplate template = context.getBean(KeyValueTemplate.class);
		Object adapter = ReflectionTestUtils.getField(template, "adapter");

		assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class);

		Object engine = ReflectionTestUtils.getField(adapter, "engine");

		assertThat(engine).isInstanceOf(SpelQueryEngine.class);
		Object sortAccessor = ReflectionTestUtils.getField(engine, "sortAccessor");

		assertThat(sortAccessor).asInstanceOf(InstanceOfAssertFactories.OPTIONAL)
				.containsInstanceOf(PathSortAccessor.class);
	}

	private static void assertKeyValueTemplateWithAdapterFor(Class<?> mapType, ApplicationContext context) {

		KeyValueTemplate template = context.getBean(KeyValueTemplate.class);
		Object adapter = ReflectionTestUtils.getField(template, "adapter");

		assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class);
		assertThat(ReflectionTestUtils.getField(adapter, "store")).isInstanceOf(mapType);
	}

	private static void assertKeyValueTemplateWithSortAccessorFor(Class<?> sortAccessorType, ApplicationContext context) {

		KeyValueTemplate template = context.getBean(KeyValueTemplate.class);
		Object adapter = ReflectionTestUtils.getField(template, "adapter");

		assertThat(adapter).isInstanceOf(MapKeyValueAdapter.class);

		Object engine = ReflectionTestUtils.getField(adapter, "engine");
		Object sortAccessor = ReflectionTestUtils.getField(engine, "sortAccessor");

		assertThat(sortAccessor).asInstanceOf(InstanceOfAssertFactories.OPTIONAL).containsInstanceOf(sortAccessorType);
	}

	@Test // GH-576
	void considersDefaultQueryCreator() {

		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);

		KeyValueRepositoryFactoryBean<?, ?, ?> factoryBean = context.getBean(KeyValueRepositoryFactoryBean.class);
		Object queryCreator = ReflectionTestUtils.getField(factoryBean, "queryCreator");

		assertThat(queryCreator).isEqualTo(PredicateQueryCreator.class);
	}

	@Test // GH-576
	void considersCustomQueryCreator() {

		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
				ConfigWithCustomQueryCreator.class);

		KeyValueRepositoryFactoryBean<?, ?, ?> factoryBean = context.getBean(KeyValueRepositoryFactoryBean.class);
		Object queryCreator = ReflectionTestUtils.getField(factoryBean, "queryCreator");

		assertThat(queryCreator).isEqualTo(MyQueryCreator.class);
	}

	@Configuration
	@EnableMapRepositories(considerNestedRepositories = true,
			includeFilters = @ComponentScan.Filter(value = PersonRepository.class, type = FilterType.ASSIGNABLE_TYPE))
	static class Config {}

	@Configuration
	@EnableMapRepositories(keyValueTemplateRef = "foo")
	static class ConfigWithCustomTemplateReference {}

	@Configuration
	@EnableMapRepositories(considerNestedRepositories = true,
			includeFilters = @ComponentScan.Filter(value = PersonRepository.class, type = FilterType.ASSIGNABLE_TYPE))
	static class ConfigWithOverriddenTemplateReference {

		@Bean
		public KeyValueOperations mapKeyValueTemplate() {
			return new KeyValueTemplate(keyValueAdapter());
		}

		@Bean
		public KeyValueAdapter keyValueAdapter() {

			KeyValueAdapter mock = mock(KeyValueAdapter.class);

			when(mock.get(any(), anyString(), any())).thenThrow(new IllegalStateException("Mock"));

			return mock;
		}

	}

	@Configuration
	@EnableMapRepositories(mapType = ConcurrentSkipListMap.class)
	static class ConfigWithCustomizedMapType {}

	@Configuration
	@EnableMapRepositories(mapType = ConcurrentSkipListMap.class)
	static class ConfigWithCustomizedMapTypeAndExplicitDefinitionOfKeyValueTemplate {

		@Bean
		public KeyValueTemplate mapKeyValueTemplate() {
			return new KeyValueTemplate(new MapKeyValueAdapter());
		}
	}

	@EnableMapRepositories(queryEngineFactory = JustSpelQueryEngineFactory.class)
	static class ConfigWithQueryEngine {}

	static class JustSpelQueryEngineFactory implements QueryEngineFactory {

		@Override
		public QueryEngine<?, ?, ?> create() {
			return new SpelQueryEngine();
		}

	}

	@EnableMapRepositories(sortAccessor = PathSortAccessor.class)
	static class ConfigWithCustomizedSortAccessor {}

	@EnableMapRepositories(sortAccessor = PathSortAccessor.class, queryEngineFactory = SpelQueryEngineFactory.class)
	static class ConfigWithQueryEngineAndCustomizedSortAccessor {}

	@EnableMapRepositories(queryCreator = MyQueryCreator.class, considerNestedRepositories = true,
			includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = PersonRepository.class))
	static class ConfigWithCustomQueryCreator {}

	static class SpelQueryEngineFactory implements QueryEngineFactory {

		private final SortAccessor<Comparator<?>> sortAccessor;

		public SpelQueryEngineFactory(SortAccessor<Comparator<?>> sortAccessor) {
			this.sortAccessor = sortAccessor;
		}

		@Override
		public QueryEngine<?, ?, ?> create() {
			return new SpelQueryEngine(sortAccessor);
		}

	}

	static class MyQueryCreator extends AbstractQueryCreator<KeyValueQuery<Predicate<?>>, Predicate<?>> {

		public MyQueryCreator(PartTree tree) {
			super(tree);
		}

		public MyQueryCreator(PartTree tree, ParameterAccessor parameters) {
			super(tree, parameters);
		}

		@Override
		protected Predicate<?> create(Part part, Iterator<Object> iterator) {
			return null;
		}

		@Override
		protected Predicate<?> and(Part part, Predicate<?> base, Iterator<Object> iterator) {
			return null;
		}

		@Override
		protected Predicate<?> or(Predicate<?> base, Predicate<?> criteria) {
			return null;
		}

		@Override
		protected KeyValueQuery<Predicate<?>> complete(Predicate<?> criteria, Sort sort) {
			return null;
		}
	}

	interface PersonRepository extends KeyValueRepository<Person, String> {

	}

	static class Person {
		@Id String id;
		String name;

		public String getId() {
			return this.id;
		}

		public String getName() {
			return this.name;
		}

		public void setId(String id) {
			this.id = id;
		}

		public void setName(String name) {
			this.name = name;
		}
	}
}