KeyValueRepositoryConfigurationExtension.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.keyvalue.repository.config;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;

import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.keyvalue.repository.KeyValueRepository;
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
import org.springframework.data.keyvalue.repository.query.SpelQueryCreator;
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean;
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.RepositoryConfigurationSource;

/**
 * {@link RepositoryConfigurationExtension} for {@link KeyValueRepository}.
 *
 * @author Christoph Strobl
 * @author Oliver Gierke
 * @author Mark Paluch
 */
public abstract class KeyValueRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {

	protected static final String MAPPING_CONTEXT_BEAN_NAME = "keyValueMappingContext";
	protected static final String KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE = "keyValueTemplateRef";

	@Override
	public String getRepositoryFactoryBeanClassName() {
		return KeyValueRepositoryFactoryBean.class.getName();
	}

	@Override
	public String getModuleName() {
		return "KeyValue";
	}

	@Override
	protected String getModulePrefix() {
		return getModuleIdentifier();
	}

	@Override
	protected Collection<Class<?>> getIdentifyingTypes() {
		return Collections.singleton(KeyValueRepository.class);
	}

	@Override
	public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) {

		AnnotationAttributes attributes = config.getAttributes();

		builder.addPropertyReference("keyValueOperations", attributes.getString(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE));
		builder.addPropertyValue("queryCreator", getQueryCreatorType(config));
		builder.addPropertyValue("queryType", getQueryType(config));
		builder.addPropertyReference("mappingContext", getMappingContextBeanRef());
	}

	/**
	 * Detects the query creator type to be used for the factory to set. Will lookup a {@link QueryCreatorType} annotation
	 * on the {@code @Enable}-annotation or use {@link SpelQueryCreator} if not found.
	 *
	 * @param config must not be {@literal null}.
	 * @return
	 */
	private static Class<?> getQueryCreatorType(AnnotationRepositoryConfigurationSource config) {

		AnnotationMetadata amd = (AnnotationMetadata) config.getSource();
		MergedAnnotation<QueryCreatorType> queryCreator = amd.getAnnotations().get(QueryCreatorType.class);
		Class<?> queryCreatorType = queryCreator.isPresent() ? queryCreator.getClass("value") : Class.class;

		if (queryCreatorType == Class.class) {
			return SpelQueryCreator.class;
		}

		return queryCreatorType;
	}

	/**
	 * Detects the query creator type to be used for the factory to set. Will lookup a {@link QueryCreatorType} annotation
	 * on the {@code @Enable}-annotation or use {@link SpelQueryCreator} if not found.
	 *
	 * @param config
	 * @return
	 */
	private static Class<?> getQueryType(AnnotationRepositoryConfigurationSource config) {

		AnnotationMetadata metadata = config.getEnableAnnotationMetadata();

		Map<String, Object> queryCreatorAnnotationAttributes = metadata
				.getAnnotationAttributes(QueryCreatorType.class.getName());

		if (queryCreatorAnnotationAttributes == null) {
			return KeyValuePartTreeQuery.class;
		}

		AnnotationAttributes queryCreatorAttributes = new AnnotationAttributes(queryCreatorAnnotationAttributes);
		return queryCreatorAttributes.getClass("repositoryQueryType");
	}

	@Override
	public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) {

		super.registerBeansForRoot(registry, configurationSource);

		registerIfNotAlreadyRegistered(() -> {

			RootBeanDefinition mappingContext = new RootBeanDefinition(KeyValueMappingContext.class);
			mappingContext.setSource(configurationSource.getSource());

			return mappingContext;

		}, registry, getMappingContextBeanRef(), configurationSource);

		Optional<String> keyValueTemplateName = configurationSource.getAttribute(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE);

		// No custom template reference configured and no matching bean definition found
		if (keyValueTemplateName.isPresent() && getDefaultKeyValueTemplateRef().equals(keyValueTemplateName.get())
				&& !registry.containsBeanDefinition(keyValueTemplateName.get())) {

			AbstractBeanDefinition beanDefinition = getDefaultKeyValueTemplateBeanDefinition(configurationSource);

			if (beanDefinition != null && configurationSource.getSource() != null) {
				registerIfNotAlreadyRegistered(() -> beanDefinition, registry, keyValueTemplateName.get(),
						configurationSource.getSource());
			}
		}
	}

	/**
	 * Get the default {@link RootBeanDefinition} for {@link org.springframework.data.keyvalue.core.KeyValueTemplate}.
	 *
	 * @return {@literal null} to explicitly not register a template.
	 * @see #getDefaultKeyValueTemplateRef()
	 */
	protected @Nullable AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(
			RepositoryConfigurationSource configurationSource) {
		return null;
	}

	/**
	 * Returns the {@link org.springframework.data.keyvalue.core.KeyValueTemplate} bean name to potentially register a
	 * default {@link org.springframework.data.keyvalue.core.KeyValueTemplate} bean if no bean is registered with the
	 * returned name.
	 *
	 * @return the default {@link org.springframework.data.keyvalue.core.KeyValueTemplate} bean name. Never
	 *         {@literal null}.
	 * @see #getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource)
	 */
	protected abstract String getDefaultKeyValueTemplateRef();

	/**
	 * Returns the {@link org.springframework.data.mapping.context.MappingContext} bean name to potentially register a
	 * default mapping context bean if no bean is registered with the returned name. Defaults to
	 * {@link MAPPING_CONTEXT_BEAN_NAME}.
	 *
	 * @return the {@link org.springframework.data.mapping.context.MappingContext} bean name. Never {@literal null}.
	 * @since 2.0
	 */
	protected String getMappingContextBeanRef() {
		return MAPPING_CONTEXT_BEAN_NAME;
	}
}