3

I have a WebMVC endpoint:

@RequestMapping(path = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> execute(@PathVariable String id) {
   ...
}

Here, the provided id should be decoded first. Is it possible to define an annotation which does this "in the background"; that is, prior to calling the endpoint? Something in the lines of:

@RequestMapping(path = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> execute(@PathVariable @DecodedIdentifier String id) {
   ...
}

Note the @DecodedIdentifier annotation. I know it does not exists, but it hopefully explains my intent. I know this is possible with Jersey's JAX-RS implementation, but what about Spring's WebMVC?

Here, I am using base64 decoding, but I wondering if I could inject a custom decoder as well.

3 Answers3

4

Although you can use annotations, I recommend you to use a custom Converter for this purpose.

Following your example, you can do something like this.

First, you need to define a custom class suitable to be converted. For instance:

public class DecodedIdentifier {
  private final String id;

  public DecodedIdentifier(String id) {
    this.id = id;
  }

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

Then, define a Converter for your custom class. It can perform the Base64 decoding:

public class DecodedIdentifierConverter implements Converter<String, DecodedIdentifier> {

  @Override
  public DecodedIdentifier convert(String source) {
    return new DecodedIdentifier(Base64.getDecoder().decode(source));
  }
}

In order to tell Spring about this converter you have several options.

If you are running Spring Boot, all you have to do is annotate the class as a @Component and the auto configuration logic will take care of Converter registration.

@Component
public class DecodedIdentifierConverter implements Converter<String, DecodedIdentifier> {

  @Override
  public DecodedIdentifier convert(String source) {
    return new DecodedIdentifier(Base64.getDecoder().decode(source));
  }
}

Be sure to configure your component scan so Spring can detect the @Component annotation in the class.

If you are using Spring MVC without Spring Boot, you need to register the Converter 'manually':

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new DecodedIdentifierConverter());
    }
}

After Converter registration, you can use it in your Controller:

@RequestMapping(path = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> execute(@PathVariable DecodedIdentifier id) {
   ...
}

There are also other options you can follow. Please, consider read this article, it will provide you further information about the problem.

As a side note, the above mentioned article indicates that you can directly define a valueOf method in the class which will store the result of the conversion service, DecodedIdentifier in your example, and it will allow you to get rid of the Converter class: to be honest, I have never tried that approach, and I do not know under which conditions it could work. Having said that, if it works, it can simplify your code. Please, if you consider it appropriate, try it.

UPDATE

Thanks to @Aman comment I carefully reviewed the Spring documentation. After that, I found that, although I think that the conversion approach aforementioned is better suited for the use case - you are actually performing a conversion - another possible solution could be the use of a custom Formatter.

I already knew that Spring uses this mechanism to perform multiple conversion but I were not aware that it is possible to register a custom formatter based on an annotation, the original idea proposed in the answer. Thinking about annotations like DateTimeFormat, it makes perfect sense. In fact, this approach were previously described here, in Stackoverflow (see the accepted answer in this question).

In your case (basically a transcription of the answer above mentioned for your case):

First, define your DecodedIdentifier annotation:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface DecodedIdentifier {
}

In fact, you can think of enriching the annotation by including, for example, the encoding in which the information should be processed.

Then, create the corresponding AnnotationFormatterFactory:

import java.text.ParseException;
import java.util.Base64;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;

import org.springframework.context.support.EmbeddedValueResolutionSupport;
import org.springframework.format.AnnotationFormatterFactory;
import org.springframework.format.Formatter;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.stereotype.Component;

@Component
public class DecodedIdentifierFormatterFactory extends EmbeddedValueResolutionSupport
  implements AnnotationFormatterFactory<DecodedIdentifier> {

  @Override
  public Set<Class<?>> getFieldTypes() {
    return Collections.singleton(String.class);
  }

  @Override
  public Printer<?> getPrinter(DecodedIdentifier annotation, Class<?> fieldType) {
    return this.getFormatter(annotation);
  }

  @Override
  public Parser<?> getParser(DecodedIdentifier annotation, Class<?> fieldType) {
    return this.getFormatter(annotation);
  }

  private Formatter getFormatter(DecodedIdentifier annotation) {
    return new Formatter<String>() {
      @Override
      public String parse(String text, Locale locale) throws ParseException {
        // If the annotation could provide some information about the
        // encoding to be used, this logic will be highly reusable
        return new String(Base64.getDecoder().decode(text));
      }

      @Override
      public String print(String object, Locale locale) {
        return object;
      }
    };
  }
}

Register the factory in your Spring MVC configuration:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatterForFieldAnnotation(new DecodedIdentifierFormatterFactory());
    }
}

Finally, use the annotation in your Controllers, exactly as you indicated in your question:

@RequestMapping(path = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> execute(@PathVariable @DecodedIdentifier String id) {
   ...
}
jccampanero
  • 50,989
  • 3
  • 20
  • 49
  • +1, a custom `Converter` is much easier and more maintainable. One note: `@PathVariable` by default takes the name of the method parameter is it annotated on, so there is no need to explicitly state `@PathVariable("id")`. IMO it's not a good practice unless required for readability purposes, as it makes refactoring harder (need to rename both). – filpa Oct 18 '20 at 13:14
  • Thank you very much for the comment. And you're right, I included it for readability, but I will update the answer and remove this unnecessary information as these comments provide some background if necessary. Again thank you very much! – jccampanero Oct 18 '20 at 13:20
  • @jccampanero But, the link you provided rather says different `The above solution with the Converter only works because we’re using Spring’s @PathVariable annotation`. – Aman Oct 24 '20 at 23:33
  • Thank you for the comment @Aman. I do not think it says nothing different. You can apply a ```Converter``` to a ```@RequestParam``` also, for instance. I think the author propose an use case like the common slug, and indicate, which is true, that with the converter approach you will need to repeat the code, it will be more boilerplate. This is why it propose another solution based on ```HandlerMethodArgumentResolver```, something similar to the one indicated in @sinuhepop answer. - Continued in the next comment – jccampanero Oct 25 '20 at 10:12
  • Spring will offer always a lot of solutions for a problem: in my answer I tried to give John, in my opinion, the more suitable and simple approach for help him to solve his problem, his use case. Having said that, please, let me carefully review the Spring docs: if necessary, I will update the answer to clarify this point. – jccampanero Oct 25 '20 at 10:13
  • @jccampanero thank you for taking the time. The article confused me into thinking that the convert way does not work unless `@PathVariable` is provided. – Aman Oct 25 '20 at 10:16
  • 1
    On the contrary @Aman, thank you, I really appreciate the comment. It always helps us review documentation and make us question things that we take to be true. – jccampanero Oct 25 '20 at 10:21
1

You can achieve this implementing a HandlerMethodArgumentResolver:

public class DecodedIdentifierArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(DecodedIdentifier.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String value = webRequest.getParameterValues(parameter.getParameterName())[0];
        return Base64.getDecoder().decode(value);
    }
}
sinuhepop
  • 20,010
  • 17
  • 72
  • 107
0

The problem with a custom HandlerMethodArgumentResolver and @PathVariable or @RequestParam is that it will never get executed, as @PathVariable and @RequestParam have their own resolvers each, which get executed prior to any custom resolvers. What if I want to obfuscate Long id param with Hashids? Then, the parameter has to be passed as a hashed String, get decoded to original Long id value. How do I provide conversion type change?

mico.barac
  • 29
  • 5