Converter from @PathVariable DomainObject to String? (using ControllerLinkBuilder.methodOn)
Asked Answered
R

4

6

I'm trying to call Spring's ControllerLinkBuilder.methodOn() with a non-String type, which always fails. And I don't know which kind of Converter to use and where to register it.

Here's my Controller:

@RestController
@RequestMapping("/companies")
class CompanyController {

    @RequestMapping(value="/{c}", method=RequestMethod.GET)
    void getIt(@PathVariable Company c) {
        System.out.println(c);
        Link link = linkTo(methodOn(getClass()).getIt(c));
    }

}

The System.out.println(c) works well. My Company Domain object get's fetched from DB. (I'm using DomainClassConverter)

But the other way doesn't work: ConverterNotFoundException: No converter found capable of converting from type @PathVariable Company to type String

Do I just need a Converter<Company, String>? And where should I register it? I tried something within the addFormatters(FormatterRegistry registry) method of WebMvcConfigurationSupport, but it did just display the same error. But after all I'm not sure what exactly I tried...

Rodroda answered 7/3, 2014 at 2:14 Comment(0)
R
0

Found a "solution". It requires a lot copy & paste from Spring's classes, but at least it works!

Basically I had to copy org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor and change two lines:

class AnnotatedParametersParameterAccessor {
    ...
    static class BoundMethodParameter {
        // OLD: (with this one you can't call addConverter())
        // private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
        // NEW:
        private static final FormattingConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();

        ...

        public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationAttribute attribute) {
            ...
            // ADD:
            CONVERSION_SERVICE.addConverter(new MyNewConverter());
    }

    ...
}

This class get's used by ControllerLinkBuilderFactory. So I had to copy & paste that, too.

And this one get's used by ControllerLinkBuilder. Also copy & paste.

My Converter just does myDomainObject.getId().toString():

public class MyNewConverter implements Converter<Company, String> {
    @Override
    public String convert(Company source) {
        return source.getId().toString();
    }   
}

Now you can use the copy&pasted ControllerLinkBuilder inside the controller and it works as expected!

Rodroda answered 8/3, 2014 at 0:26 Comment(0)
H
5

I had the same issue, it is a bug. If you don't want to do copy & paste on every controller you can try something like this in your WebMvcConfigurationSupport. It works for me.

@Override
public void addFormatters(final FormatterRegistry registry) {
    super.addFormatters(registry);

    try {
        Class<?> clazz = Class.forName("org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor$BoundMethodParameter");
        Field field = clazz.getDeclaredField("CONVERSION_SERVICE");
        field.setAccessible(true);
        DefaultFormattingConversionService service = (DefaultFormattingConversionService) field.get(null);
        for (Converter<?, ?> converter : beanFactory.getBeansOfType(Converter.class).values()) {
            service.addConverter(converter);
        }
    }
    catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}
Hillary answered 6/6, 2014 at 12:24 Comment(0)
R
0

Found a "solution". It requires a lot copy & paste from Spring's classes, but at least it works!

Basically I had to copy org.springframework.hateoas.mvc.AnnotatedParametersParameterAccessor and change two lines:

class AnnotatedParametersParameterAccessor {
    ...
    static class BoundMethodParameter {
        // OLD: (with this one you can't call addConverter())
        // private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
        // NEW:
        private static final FormattingConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();

        ...

        public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationAttribute attribute) {
            ...
            // ADD:
            CONVERSION_SERVICE.addConverter(new MyNewConverter());
    }

    ...
}

This class get's used by ControllerLinkBuilderFactory. So I had to copy & paste that, too.

And this one get's used by ControllerLinkBuilder. Also copy & paste.

My Converter just does myDomainObject.getId().toString():

public class MyNewConverter implements Converter<Company, String> {
    @Override
    public String convert(Company source) {
        return source.getId().toString();
    }   
}

Now you can use the copy&pasted ControllerLinkBuilder inside the controller and it works as expected!

Rodroda answered 8/3, 2014 at 0:26 Comment(0)
B
0

I developed a framework to render links in spring hateoas and it supports annotated parameters (@PathVariable and @RequestParam) and arbitrary parameters types.

In order to render these arbitrary types you have to create a spring bean that implements com.github.osvaldopina.linkbuilder.argumentresolver.ArgumentResolver interface.

The interface has 3 methods:

  1. public boolean resolveFor(MethodParameter methodParameter)

Is used to determine if the ArgumentResolver can be used to deal with the methodParameter. For example:

public boolean resolveFor(MethodParameter methodParameter) {
    return UserDefinedType.class.isAssignableFrom(methodParameter.getParameterType());
}

Defines that this ArgumentResover will be used for UserDefinedType.

  1. public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter)

Is used to include in the uriTemplate associated with the method the proper template parts. For example:

@Override
public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter) {
    uriTemplateAugmenter.addToQuery("value1");
    uriTemplateAugmenter.addToQuery("value2");

}

adds 2 query parameters (value1 and value2) to the uri template.

  1. public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames)

Sets in the template the values for the template variables. For example:

@Override
public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames) {
    if (parameter != null && ((UserDefinedType) parameter).getValue1() != null) {
        template.set("value1", ((UserDefinedType) parameter).getValue1());
    }
    else {
        template.set("value1", "null-value");
    }

    if (parameter != null && ((UserDefinedType) parameter).getValue2() != null) {
        template.set("value2", ((UserDefinedType) parameter).getValue2());
    }
    else {
        template.set("value2", "null-value");
    }
}

gets the UserDefinedType instance and use it to sets the templates variables value1 and value2 defined in augmentTemplate method.

A ArgumentResolver complete example would be:

@Component
public class UserDefinedTypeArgumentResolver implements ArgumentResolver {

    @Override
    public boolean resolveFor(MethodParameter methodParameter) {
        return UserDefinedType.class.isAssignableFrom(methodParameter.getParameterType());
    }

    @Override
    public void augmentTemplate(UriTemplateAugmenter uriTemplateAugmenter, MethodParameter methodParameter) {
        uriTemplateAugmenter.addToQuery("value1");
        uriTemplateAugmenter.addToQuery("value2");

    }

    @Override
    public void setTemplateVariables(UriTemplate template, MethodParameter methodParameter, Object parameter, List<String> templatedParamNames) {
        if (parameter != null && ((UserDefinedType) parameter).getValue1() != null) {
            template.set("value1", ((UserDefinedType) parameter).getValue1());
        }
        else {
            template.set("value1", "null-value");
        }

        if (parameter != null && ((UserDefinedType) parameter).getValue2() != null) {
            template.set("value2", ((UserDefinedType) parameter).getValue2());
        }
        else {
            template.set("value2", "null-value");
        }
    }
}

and for the following link builder:

   linksBuilder.link()
            .withRel("user-type")
            .fromControllerCall(RootRestController.class)
            .queryParameterForUserDefinedType(new UserDefinedType("v1", "v2"));

to the following method:

@RequestMapping("/user-defined-type")
@EnableSelfFromCurrentCall
public void queryParameterForUserDefinedType(UserDefinedType userDefinedType) {

}

would generate the following link:

{
    ...
    "_links": {
        "user-type": {
        "href": "http://localhost:8080/user-defined-type?value1=v1&value2=v2"
    }
    ...
}

}

Beetlebrowed answered 8/6, 2016 at 22:42 Comment(0)
V
0

full config in spring boot. same as Franco Gotusso's answer just provide more detail. ```

/** * This configuration file is to fix bug of Spring Hateoas. * please check https://github.com/spring-projects/spring-hateoas/issues/118. */

@Component public class MvcConfig extends WebMvcConfigurerAdapter {

@Autowired
private ApplicationContext applicationContext;

@Override
public void addFormatters(final FormatterRegistry registry) {
    super.addFormatters(registry);

    try {
        Class<?> clazz = Class.forName("org.springframework.hateoas.mvc."
                + "AnnotatedParametersParameterAccessor$BoundMethodParameter");
        Field field = clazz.getDeclaredField("CONVERSION_SERVICE");
        field.setAccessible(true);
        DefaultFormattingConversionService service =
                (DefaultFormattingConversionService) field.get(null);
        for (Formatter<?> formatter : applicationContext
                .getBeansOfType(Formatter.class).values()) {
            service.addFormatter(formatter);
        }
        for (Converter<?, ?> converter : applicationContext
                .getBeansOfType(Converter.class).values()) {
            service.addConverter(converter);
        }
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

}

```

Vizor answered 31/8, 2017 at 13:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.