2

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.

Mansur
  • 1,661
  • 3
  • 17
  • 41
  • You can look into "Java Annotation Processors" for how to implement compile time annotation processing. Spring annotation processing occurs at runtime, so yours will definitely occur first. Your example doesn't illustrate why you would want to do this. A more concrete example may help others with advice. – jeff Jul 24 '19 at 15:09
  • Thank you for the comment. What part of my example is not clear? Could you please explain, so that I can mention missing pieces as well; however, I thought I have explained the issue well. – Mansur Jul 24 '19 at 16:27
  • No problem! You mention swapping Foo with the full ApiParam but each ApiParam will have different names and values. Will each ApiParam have it's own shorthand notation? Or were you planning to use Foo for all of them? Does a single ApiParam appear in more than one place? – jeff Jul 26 '19 at 00:46
  • @jeff, added the clarification, hope it is clear now. – Mansur Jul 26 '19 at 04:20

1 Answers1

2

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!

jeff
  • 4,325
  • 16
  • 27
  • It worked!!! But why do you think it adds another level of complexity? Because I actually think changing an annotation with long parameters to an intuitive one is much more helpful. That's my thought. Could you please explain why do you think that way? Also, your solution does solve my problem in an amazing way, but returning to my generalized question, how can I replace an annotation with another in compile time. I searched for your suggestion in the first comment of yours, but I only found validation kind of stuff. – Mansur Jul 26 '19 at 04:58
  • 1
    Great! I totally agree that the smaller annotations are nicer in the method signature. I do understand the desire. But this is something custom that only your project will have vs any other Springfox project. The first time another developer encounters this they'll have to figure out how it works and how to extend it or fix it if problems occur. – jeff Jul 26 '19 at 10:13
  • 1
    There are compiler annotation processors which allow you to manipulate annotations with COMPILE RetentionPolicy. I haven't done it before though so I can't give you much more than that. I did also find other SO questions where they used "bytecode generation" to dynamically add annotations such as https://stackoverflow.com/questions/1635108/adding-java-annotations-at-runtime. Here's an article on how to write a compiler annotation processor https://medium.com/@jintin/annotation-processing-in-java-3621cb05343a – jeff Jul 26 '19 at 10:22
  • I understand, but since it is a small project, I thought it will be worth trying :). Thanks for your help, I really appreciate it. – Mansur Jul 26 '19 at 10:34