Mansur
Mansur

Reputation: 1829

Replacing an annotation with another annotation during compile time in Spring?

I am using Swagger annotations over my controller parameters. So, I end up with annotations like @ApiParam(name="default name", value="this is a default value"). I think this is quite verbose. I would like to change it to something like @Foo. I want to know if there's a way to replace @Foo with @ApiParam during compile time. Also, since I am using Spring, I think I have to consider the annotation processing order in Spring, as well. I mean I shouldn't replace @ApiParam with @Foo after Swagger or Spring picks it up. Is there any way to do this?

In simpler words, I have the same annotation with the same parameters used 5 times. Basically, I want to replace them with some custom annotation.

I know I have to show what I have already tried, but I have no clue where to even start.

Also, the question is not related to Swagger, it is just an example. I want to replace one annotation with another during compile time, so that the one picked up by Spring won't be the one I have put on the source code, but the one I have replaced.

Upvotes: 2

Views: 2205

Answers (1)

jeff
jeff

Reputation: 4333

If I understand what you are asking for, this is possible without compile-time annotation processing. It's not pretty and it might be more complexity than it's worth, but here's one way to do it.

Here's a custom annotation I made that is used for my shorthand @ApiParam.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface GameIdParam {
    String name() default "My Game ID";

    String value() default "The integer ID of a particular game";
}

You can define whatever properties in @ApiParam that you wish to override. Then you can use Springfox's Extension Framework to implement a custom handler for the new annotation.

import com.google.common.base.Optional;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import springfox.documentation.schema.Example;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.schema.EnumTypeDeterminer;
import springfox.documentation.spi.service.contexts.ParameterContext;
import springfox.documentation.spring.web.DescriptionResolver;
import springfox.documentation.swagger.readers.parameter.ApiParamParameterBuilder;

import java.util.function.Predicate;

import static java.util.Optional.ofNullable;
import static springfox.documentation.swagger.common.SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER;
import static springfox.documentation.swagger.common.SwaggerPluginSupport.pluginDoesApply;
import static springfox.documentation.swagger.readers.parameter.Examples.examples;

@Component
public class ShorthandAnnotationPlugin extends ApiParamParameterBuilder {
    private final DescriptionResolver descriptions;
    private final EnumTypeDeterminer enumTypeDeterminer;

    @Autowired
    public ShorthandAnnotationPlugin(
            DescriptionResolver descriptions,
            EnumTypeDeterminer enumTypeDeterminer) {
        super(descriptions, enumTypeDeterminer);
        this.descriptions = descriptions;
        this.enumTypeDeterminer = enumTypeDeterminer;
    }

    @Override
    public void apply(ParameterContext context) {
        Optional<GameIdParam> gameIdParam = context.resolvedMethodParameter().findAnnotation(GameIdParam.class);

        if (gameIdParam.isPresent()) {
            GameIdParam annotation = gameIdParam.get();

            // Instantiate an ApiParam so we can take default values for attributes we didn't override.
            ApiParam parentAnnotation = AnnotationUtils.synthesizeAnnotation(ApiParam.class);

            context.parameterBuilder().name(ofNullable(annotation.name())
                    .filter(((Predicate<String>) String::isEmpty).negate()).orElse(null))
                    .description(ofNullable(descriptions.resolve(annotation.value()))
                            .filter(((Predicate<String>) String::isEmpty).negate()).orElse(null))
                    .parameterAccess(ofNullable(parentAnnotation.access())
                            .filter(((Predicate<String>) String::isEmpty).negate())
                            .orElse(null))
                    .defaultValue(ofNullable(parentAnnotation.defaultValue())
                            .filter(((Predicate<String>) String::isEmpty).negate())
                            .orElse(null))
                    .allowMultiple(parentAnnotation.allowMultiple())
                    .allowEmptyValue(parentAnnotation.allowEmptyValue())
                    .required(parentAnnotation.required())
                    .scalarExample(new Example(parentAnnotation.example()))
                    .complexExamples(examples(parentAnnotation.examples()))
                    .hidden(parentAnnotation.hidden())
                    .collectionFormat(parentAnnotation.collectionFormat())
                    .order(SWAGGER_PLUGIN_ORDER);
        }
    }

    @Override
    public boolean supports(DocumentationType documentationType) {
        return pluginDoesApply(documentationType);
    }
}

I used Springfox's ApiParamParameterBuilder as an example.

Now, I can use my @GameIdParam

@PostMapping("/{gameId}/info")
public String play(@GameIdParam @PathVariable int gameId) // ...

This pattern could be generalized to work with a series of custom shorthand annotations. It's not pretty and it introduces another level of indirection that people who know Springfox Swagger won't be familiar with.

Hope that helps! Good luck!

Upvotes: 2

Related Questions