All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.dhi13man.spring.datasource.processor.TargetDataSourceAnnotationProcessor Maven / Gradle / Ivy

Go to download

To mitigate Spring's limitations with multiple data sources in a single service, this library provides two custom annotations in Java that automatically generate all the required bean definition configurations and package-segregated repositories, for each data source.

The newest version!
package io.github.dhi13man.spring.datasource.processor;

import static io.github.dhi13man.spring.datasource.constants.MultiDataSourceErrorConstants.NO_REPOSITORY_METHOD_ANNOTATED_WITH_TARGET_SECONDARY_DATA_SOURCE;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import io.github.dhi13man.spring.datasource.annotations.TargetSecondaryDataSource;
import io.github.dhi13man.spring.datasource.annotations.TargetSecondaryDataSources;
import io.github.dhi13man.spring.datasource.generators.MultiDataSourceConfigGenerator;
import io.github.dhi13man.spring.datasource.generators.MultiDataSourceRepositoryGenerator;
import io.github.dhi13man.spring.datasource.utils.MultiDataSourceCommonStringUtils;
import io.github.dhi13man.spring.datasource.utils.MultiDataSourceGeneratorUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;

/**
 * Annotation processor to  create copies of the repositories in relevant packages for all the
 * repositories annotated with  {@link TargetSecondaryDataSource}
 */
@AutoService(Processor.class)
public class TargetDataSourceAnnotationProcessor extends AbstractProcessor {

  public static final String GENERATED_REPOSITORIES_PACKAGE_SUFFIX = ".generated.repositories";

  private static final String ERROR_WHILE_WRITING_THE_CLASS = "Error while writing the class: ";

  private Filer filer;

  private Messager messager;

  private Elements elementUtils;

  private Types typeUtils;

  private MultiDataSourceCommonStringUtils commonStringUtils;

  private MultiDataSourceGeneratorUtils generatorUtils;

  private MultiDataSourceConfigGenerator configGenerator;

  private MultiDataSourceRepositoryGenerator repositoryGenerator;

  /**
   * Constructor for the annotation processor to be run during compile time.
   */
  public TargetDataSourceAnnotationProcessor() {
  }

  /**
   * Constructor for the annotation processor with dependency injection.
   * 

* This constructor is used for testing purposes. * * @param filer the filer to use for writing files * @param messager the messager to use for printing messages * @param elementUtils the element utils to use for getting packages * @param typeUtils the type utils to use for getting types * @param commonStringUtils Utility class for common string operations * @param generatorUtils Utility class for generating code for the Multi Data Source library * @param configGenerator the Multi Data Source config generator * @param repositoryGenerator the Multi Data Source repository generator */ public TargetDataSourceAnnotationProcessor( Filer filer, Messager messager, Elements elementUtils, Types typeUtils, MultiDataSourceCommonStringUtils commonStringUtils, MultiDataSourceGeneratorUtils generatorUtils, MultiDataSourceConfigGenerator configGenerator, MultiDataSourceRepositoryGenerator repositoryGenerator ) { this.filer = filer; this.messager = messager; this.elementUtils = elementUtils; this.typeUtils = typeUtils; this.commonStringUtils = commonStringUtils; this.generatorUtils = generatorUtils; this.configGenerator = configGenerator; this.repositoryGenerator = repositoryGenerator; } /** * {@inheritDoc} *

* Prepares the {@link Filer}, {@link Messager} and {@link Elements} for use in the processor. * * @param processingEnv environment to access facilities the tool framework provides to the * processor */ @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.filer = Objects.nonNull(this.filer) ? this.filer : processingEnv.getFiler(); this.messager = Objects.nonNull(this.messager) ? this.messager : processingEnv.getMessager(); this.elementUtils = Objects.nonNull(this.elementUtils) ? this.elementUtils : processingEnv.getElementUtils(); this.typeUtils = Objects.nonNull(this.typeUtils) ? this.typeUtils : processingEnv.getTypeUtils(); this.commonStringUtils = Objects.nonNull(this.commonStringUtils) ? this.commonStringUtils : MultiDataSourceCommonStringUtils.getInstance(); this.generatorUtils = Objects.nonNull(this.generatorUtils) ? this.generatorUtils : MultiDataSourceGeneratorUtils.getInstance(); this.configGenerator = Objects.nonNull(this.configGenerator) ? this.configGenerator : new MultiDataSourceConfigGenerator(this.generatorUtils, this.commonStringUtils); this.repositoryGenerator = Objects.nonNull(this.repositoryGenerator) ? this.repositoryGenerator : new MultiDataSourceRepositoryGenerator( this.messager, processingEnv.getTypeUtils(), this.commonStringUtils, this.generatorUtils ); } /** * {@inheritDoc} *

* Performs the following tasks for each {@link TargetSecondaryDataSource} annotated repository * method: *

* 1. Aggregates the list of data sources to be used throughout the system *

* 2. Generates configs to create relevant beans for each of the data sources. *

* 3. Creates copies of the repositories with only the annotated methods in the relevant packages, * for package segregated data source injection which is required for multiple data source support * in Spring. * * @param annotations the annotation types requested to be processed * @param roundEnv environment for information about the current and prior round * @return whether the set of annotations are claimed by this processor */ @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { final Map> dataSourceToTargetRepositoryMethodMap = this .createDataSourceToTargetRepositoryMethodMap(roundEnv); if (dataSourceToTargetRepositoryMethodMap.isEmpty()) { messager.printMessage( Kind.NOTE, NO_REPOSITORY_METHOD_ANNOTATED_WITH_TARGET_SECONDARY_DATA_SOURCE ); return false; } // Process the target executable elements to produce the alternate data source config classes for (final var executableElementsEntry : dataSourceToTargetRepositoryMethodMap.entrySet()) { // Get the relevant details for this data source final String dataSourceName = executableElementsEntry.getKey(); final Set executableElements = executableElementsEntry.getValue(); // Create map of type elements (repositories) to executable elements (methods) for this source final Map> repositoryToMethodMap = this .createTypeElementToExecutableElementsMap(executableElements); repositoryToMethodMap.forEach((k, v) -> this.generateRepositories(k, v, dataSourceName)); final String generatedInfoString = executableElements.size() + " Repositories for data source " + dataSourceName + " generated."; messager.printMessage(Kind.NOTE, generatedInfoString); } // As per sonatype, return false to indicate that the annotation processor is not claiming // the annotations: https://errorprone.info/bugpattern/DoNotClaimAnnotations return false; } @Override public Set getSupportedAnnotationTypes() { return Set.of( TargetSecondaryDataSource.class.getCanonicalName(), TargetSecondaryDataSources.class.getCanonicalName() ); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } /** * Generate the repositories for the given type element and annotated methods. *

* Creates a copy of the repository in the relevant package with only the annotated methods. * * @param typeElement the type element for the source repository * @param annotatedMethods the set of annotated methods for the source repository that need to be * copied for the target data source * @param dataSourceName the name of the target data source to generate the repository for */ private void generateRepositories( @Nonnull TypeElement typeElement, @Nonnull Set annotatedMethods, @Nonnull String dataSourceName ) { // Generate the repository type element with only the annotated methods as allowed final TypeSpec copiedTypeSpec = repositoryGenerator.generateRepositoryTypeElementWithAnnotatedMethods( typeElement, annotatedMethods, dataSourceName ); final PackageElement elementPackage = elementUtils.getPackageOf(typeElement); final String repositoryDataSourceSubPackage = this .generateNonPrimaryDataSourceRepositoryPackage(elementPackage, dataSourceName); writeTypeSpecToPackage(repositoryDataSourceSubPackage, copiedTypeSpec); } /** * Creates a map of the data source name to the set of ExecutableElements that are annotated with * {@link TargetSecondaryDataSource} for that data source. *

* Targets all Classes annotated with {@link TargetSecondaryDataSource} or its container * annotation. Return a map grouped by the data source name to the set of ExecutableElements that * are annotated with {@link TargetSecondaryDataSource} for that data source. * * @param roundEnv environment for information about the current and prior round * @return map of the data source name to the set of ExecutableElements that are annotated with */ private @Nonnull Map> createDataSourceToTargetRepositoryMethodMap( @Nonnull RoundEnvironment roundEnv ) { // Deal with individual @TargetSecondaryDataSource annotations final Map> targetDataSourceAnnotatedMethodMap = roundEnv .getElementsAnnotatedWith(TargetSecondaryDataSource.class) .stream() .filter(element -> element instanceof ExecutableElement) .map(ExecutableElement.class::cast) .collect( Collectors.groupingBy( x -> x.getAnnotation(TargetSecondaryDataSource.class).value(), Collectors.toSet() ) ); // Deal with @TargetSecondaryDataSources container annotations final Set annotatedElements = roundEnv .getElementsAnnotatedWith(TargetSecondaryDataSources.class) .stream() .filter(element -> element instanceof ExecutableElement) .map(ExecutableElement.class::cast) .collect(Collectors.toSet()); for (final ExecutableElement element : annotatedElements) { final TargetSecondaryDataSources repositoriesAnnotation = element .getAnnotation(TargetSecondaryDataSources.class); final List dataSourcesInvolved = Arrays.stream(repositoriesAnnotation.value()) .map(TargetSecondaryDataSource::value) .collect(Collectors.toList()); for (final String dataSourceName : dataSourcesInvolved) { final Set executableElements = targetDataSourceAnnotatedMethodMap .getOrDefault(dataSourceName, new HashSet<>()); executableElements.add(element); targetDataSourceAnnotatedMethodMap.put(dataSourceName, executableElements); } } return targetDataSourceAnnotatedMethodMap; } /** * Creates a map of the {@link TypeElement} to the set of {@link ExecutableElement}s that are * annotated with {@link TargetSecondaryDataSource}. * * @param executableElements set of {@link ExecutableElement}s that are annotated with * {@link TargetSecondaryDataSource} * @return map of the {@link TypeElement} to the set of {@link ExecutableElement}s that are * annotated with {@link TargetSecondaryDataSource} */ private @Nonnull Map> createTypeElementToExecutableElementsMap( @Nonnull Set executableElements ) { return executableElements.stream().collect( Collectors.groupingBy(x -> (TypeElement) x.getEnclosingElement(), Collectors.toSet()) ); } /** * Write a {@link TypeSpec} to a package using the {@link Filer}. * * @param targetPackage the package to write the {@link TypeSpec} to * @param typeSpec the {@link TypeSpec} to write */ private void writeTypeSpecToPackage(@Nonnull String targetPackage, @Nonnull TypeSpec typeSpec) { try { JavaFile.builder(targetPackage, typeSpec).build().writeTo(filer); } catch (IOException e) { messager.printMessage(Kind.ERROR, ERROR_WHILE_WRITING_THE_CLASS + e); throw new IllegalStateException(ERROR_WHILE_WRITING_THE_CLASS + e); } } /** * Generate the package name for the non-primary data source repositories. * * @param elementPackage the package element of the source repository * @param dataSourceName the name of the data source to generate the repository package for * @return the package name for the non-primary data source repositories */ private @Nonnull String generateNonPrimaryDataSourceRepositoryPackage( @Nonnull PackageElement elementPackage, @Nonnull String dataSourceName ) { return elementPackage + GENERATED_REPOSITORIES_PACKAGE_SUFFIX + "." + commonStringUtils.toSnakeCase(dataSourceName); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy